mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 14:35:41 +00:00
perf(bundle): lazy-load rarely-visited pages + split vendor chunks (#249)
Cold-start bundle was a single 1.88 MB / 577 KB-gzipped chunk that
parsed every page in the app before first paint, including pages a
typical session never opens.
Two-part fix:
1. Lazy-load 10 rarely-visited pages via React.lazy() — each becomes
its own chunk that only downloads when the route is hit:
Settings, Statistics, Help, WhatsNew, DeviceSync, OfflineLibrary,
LabelAlbums, AdvancedSearch, FolderBrowser, InternetRadio.
Whole <Routes> tree is wrapped in <Suspense fallback={null}> — no
visible loading state, the browser cache hits on subsequent visits.
2. Vite manualChunks splits dependencies that change rarely from app
code: react/react-dom/react-router, the @tauri-apps/* family, and
i18next. Tauri auto-updater pulls smaller deltas when only app
code changed (vendor chunks stay byte-identical and cached).
Build output:
Before: 1.88 MB / 577 KB gz monolithic
After cold start: ~450 KB gz (index 370 + react 54 + tauri 6 + i18n 20)
= 22% smaller initial download
Plus 11 lazy chunks. Notably WhatsNew's bundled CHANGELOG (215 KB /
76 KB gz) is now off the cold-start path entirely.
Co-authored-by: Psychotoxical <dev@psysonic.app>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
88cd09c0be
commit
aa0776e811
+46
-40
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState, useCallback, useRef } from 'react';
|
import React, { useEffect, useState, useCallback, useRef, lazy, Suspense } from 'react';
|
||||||
import { showToast } from './utils/toast';
|
import { showToast } from './utils/toast';
|
||||||
import { BrowserRouter, Routes, Route, Navigate, useNavigate, useLocation } from 'react-router-dom';
|
import { BrowserRouter, Routes, Route, Navigate, useNavigate, useLocation } from 'react-router-dom';
|
||||||
import { listen } from '@tauri-apps/api/event';
|
import { listen } from '@tauri-apps/api/event';
|
||||||
@@ -14,6 +14,7 @@ import { useIsMobile } from './hooks/useIsMobile';
|
|||||||
import LiveSearch from './components/LiveSearch';
|
import LiveSearch from './components/LiveSearch';
|
||||||
import NowPlayingDropdown from './components/NowPlayingDropdown';
|
import NowPlayingDropdown from './components/NowPlayingDropdown';
|
||||||
import QueuePanel from './components/QueuePanel';
|
import QueuePanel from './components/QueuePanel';
|
||||||
|
// Eager — main browsing flow, loaded on first paint
|
||||||
import Home from './pages/Home';
|
import Home from './pages/Home';
|
||||||
import Albums from './pages/Albums';
|
import Albums from './pages/Albums';
|
||||||
import Artists from './pages/Artists';
|
import Artists from './pages/Artists';
|
||||||
@@ -22,23 +23,27 @@ import NewReleases from './pages/NewReleases';
|
|||||||
import Favorites from './pages/Favorites';
|
import Favorites from './pages/Favorites';
|
||||||
import RandomMix from './pages/RandomMix';
|
import RandomMix from './pages/RandomMix';
|
||||||
import RandomLanding from './pages/RandomLanding';
|
import RandomLanding from './pages/RandomLanding';
|
||||||
import Settings from './pages/Settings';
|
|
||||||
import Login from './pages/Login';
|
import Login from './pages/Login';
|
||||||
import AlbumDetail from './pages/AlbumDetail';
|
import AlbumDetail from './pages/AlbumDetail';
|
||||||
import LabelAlbums from './pages/LabelAlbums';
|
|
||||||
import Statistics from './pages/Statistics';
|
|
||||||
import MostPlayed from './pages/MostPlayed';
|
import MostPlayed from './pages/MostPlayed';
|
||||||
import Help from './pages/Help';
|
|
||||||
import RandomAlbums from './pages/RandomAlbums';
|
import RandomAlbums from './pages/RandomAlbums';
|
||||||
import SearchResults from './pages/SearchResults';
|
import SearchResults from './pages/SearchResults';
|
||||||
import AdvancedSearch from './pages/AdvancedSearch';
|
|
||||||
import Playlists from './pages/Playlists';
|
import Playlists from './pages/Playlists';
|
||||||
import PlaylistDetail from './pages/PlaylistDetail';
|
import PlaylistDetail from './pages/PlaylistDetail';
|
||||||
import InternetRadio from './pages/InternetRadio';
|
|
||||||
import FolderBrowser from './pages/FolderBrowser';
|
|
||||||
import DeviceSync from './pages/DeviceSync';
|
|
||||||
import NowPlayingPage from './pages/NowPlaying';
|
import NowPlayingPage from './pages/NowPlaying';
|
||||||
import WhatsNew from './pages/WhatsNew';
|
|
||||||
|
// Lazy — visited rarely or on-demand. Each becomes its own chunk so the
|
||||||
|
// initial bundle stays smaller and these pages don't block first paint.
|
||||||
|
const Settings = lazy(() => import('./pages/Settings'));
|
||||||
|
const Statistics = lazy(() => import('./pages/Statistics'));
|
||||||
|
const Help = lazy(() => import('./pages/Help'));
|
||||||
|
const WhatsNew = lazy(() => import('./pages/WhatsNew'));
|
||||||
|
const DeviceSync = lazy(() => import('./pages/DeviceSync'));
|
||||||
|
const OfflineLibrary = lazy(() => import('./pages/OfflineLibrary'));
|
||||||
|
const LabelAlbums = lazy(() => import('./pages/LabelAlbums'));
|
||||||
|
const AdvancedSearch = lazy(() => import('./pages/AdvancedSearch'));
|
||||||
|
const FolderBrowser = lazy(() => import('./pages/FolderBrowser'));
|
||||||
|
const InternetRadio = lazy(() => import('./pages/InternetRadio'));
|
||||||
import MiniPlayer from './components/MiniPlayer';
|
import MiniPlayer from './components/MiniPlayer';
|
||||||
import { initMiniPlayerBridgeOnMain } from './utils/miniPlayerBridge';
|
import { initMiniPlayerBridgeOnMain } from './utils/miniPlayerBridge';
|
||||||
import FullscreenPlayer from './components/FullscreenPlayer';
|
import FullscreenPlayer from './components/FullscreenPlayer';
|
||||||
@@ -50,7 +55,6 @@ import TooltipPortal from './components/TooltipPortal';
|
|||||||
import ConnectionIndicator from './components/ConnectionIndicator';
|
import ConnectionIndicator from './components/ConnectionIndicator';
|
||||||
import LastfmIndicator from './components/LastfmIndicator';
|
import LastfmIndicator from './components/LastfmIndicator';
|
||||||
import OfflineBanner from './components/OfflineBanner';
|
import OfflineBanner from './components/OfflineBanner';
|
||||||
import OfflineLibrary from './pages/OfflineLibrary';
|
|
||||||
import Genres from './pages/Genres';
|
import Genres from './pages/Genres';
|
||||||
import GenreDetail from './pages/GenreDetail';
|
import GenreDetail from './pages/GenreDetail';
|
||||||
import ExportPickerModal from './components/ExportPickerModal';
|
import ExportPickerModal from './components/ExportPickerModal';
|
||||||
@@ -401,35 +405,37 @@ function AppShell() {
|
|||||||
<OfflineBanner onRetry={connRetry} isChecking={connRetrying} showSettingsLink={!hasOfflineContent} serverName={serverName} />
|
<OfflineBanner onRetry={connRetry} isChecking={connRetrying} showSettingsLink={!hasOfflineContent} serverName={serverName} />
|
||||||
)}
|
)}
|
||||||
<div className="content-body" style={{ padding: 0, position: 'relative' }}>
|
<div className="content-body" style={{ padding: 0, position: 'relative' }}>
|
||||||
<Routes>
|
<Suspense fallback={null}>
|
||||||
<Route path="/" element={<Home />} />
|
<Routes>
|
||||||
<Route path="/albums" element={<Albums />} />
|
<Route path="/" element={<Home />} />
|
||||||
<Route path="/random" element={<RandomLanding />} />
|
<Route path="/albums" element={<Albums />} />
|
||||||
<Route path="/random/albums" element={<RandomAlbums />} />
|
<Route path="/random" element={<RandomLanding />} />
|
||||||
<Route path="/album/:id" element={<AlbumDetail />} />
|
<Route path="/random/albums" element={<RandomAlbums />} />
|
||||||
<Route path="/artists" element={<Artists />} />
|
<Route path="/album/:id" element={<AlbumDetail />} />
|
||||||
<Route path="/artist/:id" element={<ArtistDetail />} />
|
<Route path="/artists" element={<Artists />} />
|
||||||
<Route path="/new-releases" element={<NewReleases />} />
|
<Route path="/artist/:id" element={<ArtistDetail />} />
|
||||||
<Route path="/favorites" element={<Favorites />} />
|
<Route path="/new-releases" element={<NewReleases />} />
|
||||||
<Route path="/random/mix" element={<RandomMix />} />
|
<Route path="/favorites" element={<Favorites />} />
|
||||||
<Route path="/label/:name" element={<LabelAlbums />} />
|
<Route path="/random/mix" element={<RandomMix />} />
|
||||||
<Route path="/search" element={<SearchResults />} />
|
<Route path="/label/:name" element={<LabelAlbums />} />
|
||||||
<Route path="/search/advanced" element={<AdvancedSearch />} />
|
<Route path="/search" element={<SearchResults />} />
|
||||||
<Route path="/statistics" element={<Statistics />} />
|
<Route path="/search/advanced" element={<AdvancedSearch />} />
|
||||||
<Route path="/most-played" element={<MostPlayed />} />
|
<Route path="/statistics" element={<Statistics />} />
|
||||||
<Route path="/now-playing" element={isMobile ? <MobilePlayerView /> : <NowPlayingPage />} />
|
<Route path="/most-played" element={<MostPlayed />} />
|
||||||
<Route path="/settings" element={<Settings />} />
|
<Route path="/now-playing" element={isMobile ? <MobilePlayerView /> : <NowPlayingPage />} />
|
||||||
<Route path="/whats-new" element={<WhatsNew />} />
|
<Route path="/settings" element={<Settings />} />
|
||||||
<Route path="/help" element={<Help />} />
|
<Route path="/whats-new" element={<WhatsNew />} />
|
||||||
<Route path="/offline" element={<OfflineLibrary />} />
|
<Route path="/help" element={<Help />} />
|
||||||
<Route path="/genres" element={<Genres />} />
|
<Route path="/offline" element={<OfflineLibrary />} />
|
||||||
<Route path="/genres/:name" element={<GenreDetail />} />
|
<Route path="/genres" element={<Genres />} />
|
||||||
<Route path="/playlists" element={<Playlists />} />
|
<Route path="/genres/:name" element={<GenreDetail />} />
|
||||||
<Route path="/playlists/:id" element={<PlaylistDetail />} />
|
<Route path="/playlists" element={<Playlists />} />
|
||||||
<Route path="/radio" element={<InternetRadio />} />
|
<Route path="/playlists/:id" element={<PlaylistDetail />} />
|
||||||
<Route path="/folders" element={<FolderBrowser />} />
|
<Route path="/radio" element={<InternetRadio />} />
|
||||||
<Route path="/device-sync" element={<DeviceSync />} />
|
<Route path="/folders" element={<FolderBrowser />} />
|
||||||
</Routes>
|
<Route path="/device-sync" element={<DeviceSync />} />
|
||||||
|
</Routes>
|
||||||
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -27,5 +27,25 @@ export default defineConfig({
|
|||||||
target: process.env.TAURI_ENV_PLATFORM === "windows" ? "chrome105" : "safari13",
|
target: process.env.TAURI_ENV_PLATFORM === "windows" ? "chrome105" : "safari13",
|
||||||
minify: !process.env.TAURI_ENV_DEBUG ? "esbuild" : false,
|
minify: !process.env.TAURI_ENV_DEBUG ? "esbuild" : false,
|
||||||
sourcemap: !!process.env.TAURI_ENV_DEBUG,
|
sourcemap: !!process.env.TAURI_ENV_DEBUG,
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
// Vendor chunks isolate dependencies that change rarely from app code,
|
||||||
|
// so a normal app update doesn't invalidate the cached vendor bundles
|
||||||
|
// (helps especially with the Tauri updater pulling deltas).
|
||||||
|
manualChunks: {
|
||||||
|
react: ["react", "react-dom", "react-router-dom"],
|
||||||
|
tauri: [
|
||||||
|
"@tauri-apps/api",
|
||||||
|
"@tauri-apps/plugin-shell",
|
||||||
|
"@tauri-apps/plugin-dialog",
|
||||||
|
"@tauri-apps/plugin-fs",
|
||||||
|
"@tauri-apps/plugin-process",
|
||||||
|
"@tauri-apps/plugin-store",
|
||||||
|
"@tauri-apps/plugin-updater",
|
||||||
|
],
|
||||||
|
i18n: ["i18next", "react-i18next"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user