perf: lazy-load cache-manager to reduce initial bundle size#256
perf: lazy-load cache-manager to reduce initial bundle size#256
Conversation
- Dynamic import cache-manager in upload-form and library-service - Add idle-time prefetch mechanism using requestIdleCallback - Reduces cache-manager chunk from 105KB to 1.3KB in initial load - music-metadata (~100KB) now loads only when needed Improves TBT from 90ms to 60ms (-33%) Fixes CLS from 0.114 to 0 Part of #220
There was a problem hiding this comment.
Pull request overview
This PR implements lazy-loading for the cache-manager module to reduce the initial bundle size by ~100KB and improve page load performance. The module is now dynamically imported only when needed (during file uploads or library deletion), with an idle-time prefetching strategy to ensure it's cached before users typically need it.
Key Changes:
- Dynamic imports replace static imports for
cache-managerin upload and library deletion flows - New
prefetch.tsmodule usesrequestIdleCallbackto preload heavy modules during browser idle time - Prefetching is initialized in
AuthProviderto warm the cache 3 seconds after app mount
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
frontend/src/lib/services/library-service.ts |
Replaces static import with lazy-loaded getCacheNameLazy function for cache operations during library deletion |
frontend/src/lib/prefetch.ts |
New module implementing idle-time prefetching strategy using requestIdleCallback with setTimeout fallback |
frontend/src/components/providers/auth-provider.tsx |
Initializes prefetch module on component mount to preload heavy modules after initial render |
frontend/src/components/features/upload/upload-form/index.tsx |
Converts static import to dynamic import for caching functions, loading only when files are uploaded |
| // Start idle-time prefetch of heavy modules after initial render | ||
| useEffect(() => { | ||
| startIdlePrefetch(); | ||
| }, []); |
There was a problem hiding this comment.
The startIdlePrefetch function is called unconditionally in a useEffect with an empty dependency array, which means it will be called every time the AuthProvider mounts. However, the prefetchModule function already has a guard (prefetchedModules Set) that prevents duplicate prefetching. This guard won't work correctly if the module is ever unmounted and remounted because startIdlePrefetch() creates a new setTimeout each time. Consider either: 1) ensuring startIdlePrefetch itself checks if prefetching is already in progress/completed, or 2) documenting that the guard in prefetchModule is intentional and sufficient.
| let cachedGetCacheName: ((type: "audio" | "covers") => string) | null = null; | ||
| async function getCacheNameLazy(type: "audio" | "covers"): Promise<string> { | ||
| if (!cachedGetCacheName) { | ||
| const module = await import("@/lib/pwa/cache-manager"); | ||
| cachedGetCacheName = module.getCacheName; | ||
| } | ||
| return cachedGetCacheName(type); |
There was a problem hiding this comment.
The getCacheNameLazy function caches the getCacheName function reference in a module-level variable. Since getCacheName is a simple function that just returns a constant (CACHE_NAME), this caching adds unnecessary complexity. The overhead saved is negligible compared to the dynamic import that's already happening. Consider simplifying by returning the getCacheName function call directly: return (await import("@/lib/pwa/cache-manager")).getCacheName(type); without the cachedGetCacheName variable.
| let cachedGetCacheName: ((type: "audio" | "covers") => string) | null = null; | |
| async function getCacheNameLazy(type: "audio" | "covers"): Promise<string> { | |
| if (!cachedGetCacheName) { | |
| const module = await import("@/lib/pwa/cache-manager"); | |
| cachedGetCacheName = module.getCacheName; | |
| } | |
| return cachedGetCacheName(type); | |
| async function getCacheNameLazy(type: "audio" | "covers"): Promise<string> { | |
| return (await import("@/lib/pwa/cache-manager")).getCacheName(type); |
| if ("requestIdleCallback" in window) { | ||
| requestIdleCallback(callback, { timeout: 5000 }); |
There was a problem hiding this comment.
TypeScript does not have built-in type definitions for requestIdleCallback. This code will cause a TypeScript compilation error. You need to add type declarations for requestIdleCallback to the project's type definitions file (e.g., vite-env.d.ts) to properly type this API.
|
Coverage after merging perf/lazy-load-cache-manager into main will be
Coverage Report |
Summary
Lazy-load the
cache-managermodule (which containsmusic-metadata~100KB) to improve initial page load performance.Changes
cache-managerin:upload-form/index.tsx- only loads when user uploads fileslibrary-service.ts- only loads when user deletes a libraryprefetch.ts):requestIdleCallbackto preload modules 3 seconds after app mountPerformance Results
Note: FCP remains at 2.4s - the bottleneck is the main bundle size (383KB), not cache-manager.
Testing
Closes #220