From aa0776e8119132dd09d75bb966024b5afd381e49 Mon Sep 17 00:00:00 2001 From: Frank Stellmacher Date: Tue, 21 Apr 2026 21:38:07 +0200 Subject: [PATCH] perf(bundle): lazy-load rarely-visited pages + split vendor chunks (#249) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 tree is wrapped in — 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 Co-authored-by: Claude Opus 4.7 (1M context) --- src/App.tsx | 86 +++++++++++++++++++++++++++----------------------- vite.config.ts | 20 ++++++++++++ 2 files changed, 66 insertions(+), 40 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index dec5a283..4c19dd64 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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 { BrowserRouter, Routes, Route, Navigate, useNavigate, useLocation } from 'react-router-dom'; import { listen } from '@tauri-apps/api/event'; @@ -14,6 +14,7 @@ import { useIsMobile } from './hooks/useIsMobile'; import LiveSearch from './components/LiveSearch'; import NowPlayingDropdown from './components/NowPlayingDropdown'; import QueuePanel from './components/QueuePanel'; +// Eager — main browsing flow, loaded on first paint import Home from './pages/Home'; import Albums from './pages/Albums'; import Artists from './pages/Artists'; @@ -22,23 +23,27 @@ import NewReleases from './pages/NewReleases'; import Favorites from './pages/Favorites'; import RandomMix from './pages/RandomMix'; import RandomLanding from './pages/RandomLanding'; -import Settings from './pages/Settings'; import Login from './pages/Login'; import AlbumDetail from './pages/AlbumDetail'; -import LabelAlbums from './pages/LabelAlbums'; -import Statistics from './pages/Statistics'; import MostPlayed from './pages/MostPlayed'; -import Help from './pages/Help'; import RandomAlbums from './pages/RandomAlbums'; import SearchResults from './pages/SearchResults'; -import AdvancedSearch from './pages/AdvancedSearch'; import Playlists from './pages/Playlists'; 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 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 { initMiniPlayerBridgeOnMain } from './utils/miniPlayerBridge'; import FullscreenPlayer from './components/FullscreenPlayer'; @@ -50,7 +55,6 @@ import TooltipPortal from './components/TooltipPortal'; import ConnectionIndicator from './components/ConnectionIndicator'; import LastfmIndicator from './components/LastfmIndicator'; import OfflineBanner from './components/OfflineBanner'; -import OfflineLibrary from './pages/OfflineLibrary'; import Genres from './pages/Genres'; import GenreDetail from './pages/GenreDetail'; import ExportPickerModal from './components/ExportPickerModal'; @@ -401,35 +405,37 @@ function AppShell() { )}
- - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - : } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + : } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + +
diff --git a/vite.config.ts b/vite.config.ts index 8ab53c0e..56ee0385 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -27,5 +27,25 @@ export default defineConfig({ target: process.env.TAURI_ENV_PLATFORM === "windows" ? "chrome105" : "safari13", minify: !process.env.TAURI_ENV_DEBUG ? "esbuild" : false, 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"], + }, + }, + }, }, });