diff --git a/client/iconChunk.js b/client/iconChunk.js new file mode 100644 index 000000000..61ed53cdd --- /dev/null +++ b/client/iconChunk.js @@ -0,0 +1,18 @@ +// The chunk group that holds the lucide icons the app loads at startup (vite.config.js). +// +// Every lucide icon is a module of its own, and the plugin icon lookup +// (src/components/shared/PluginIcon.tsx) reaches all of them by name, from the navigation +// every page renders. Left to itself the bundler puts each icon that some other chunk +// also uses into a chunk of its own, so that neither side holds a copy: 187 files of a few +// hundred bytes each, every one a request of its own on a first visit and an entry of its +// own in the service worker's precache. In one group they are one file, whose hash changes +// only when lucide-react does. +// +// `tags: ['$initial']` for the reason the other groups carry it: without it the group would +// also take icons that today sit behind a lazy route and make them part of the first load. +export const ICON_CHUNK = { + name: 'vendor-icons', + priority: 20, + tags: ['$initial'], + test: /[\\/]node_modules[\\/]lucide-react[\\/]/, +}; diff --git a/client/tests/unit/pwa/iconChunk.test.ts b/client/tests/unit/pwa/iconChunk.test.ts new file mode 100644 index 000000000..0d4a4e6fb --- /dev/null +++ b/client/tests/unit/pwa/iconChunk.test.ts @@ -0,0 +1,33 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { ICON_CHUNK } from '../../../iconChunk.js'; + +describe('ICON_CHUNK', () => { + it('FE-ICON-CHUNK-001: takes every lucide module, on either path separator', () => { + expect(ICON_CHUNK.test.test('/app/node_modules/lucide-react/dist/esm/icons/chevron-down.js')).toBe(true); + expect(ICON_CHUNK.test.test('/app/node_modules/lucide-react/dist/esm/createLucideIcon.js')).toBe(true); + expect(ICON_CHUNK.test.test('C:\\app\\node_modules\\lucide-react\\dist\\esm\\icons\\map.js')).toBe(true); + }); + + it('FE-ICON-CHUNK-002: takes nothing else', () => { + expect(ICON_CHUNK.test.test('/app/node_modules/react/index.js')).toBe(false); + expect(ICON_CHUNK.test.test('/app/node_modules/lucide-react-native/dist/esm/icons/map.js')).toBe(false); + expect(ICON_CHUNK.test.test('/app/client/src/components/shared/PluginIcon.tsx')).toBe(false); + expect(ICON_CHUNK.test.test('/app/client/src/lucide-react/own.ts')).toBe(false); + }); + + it('FE-ICON-CHUNK-003: holds only what the first load already needs', () => { + // Without the tag the group would also collect icons that sit behind a lazy route + // and make them part of the first load (see the note on the groups in vite.config.js). + expect(ICON_CHUNK.tags).toEqual(['$initial']); + expect(ICON_CHUNK.name).toBe('vendor-icons'); + }); + + it('FE-ICON-CHUNK-004: vite.config.js lists it among the chunk groups', () => { + const config = readFileSync(resolve(process.cwd(), 'vite.config.js'), 'utf8'); + const groups = config.slice(config.indexOf('groups: [')); + expect(config).toContain("import { ICON_CHUNK } from './iconChunk.js'"); + expect(groups).toMatch(/groups: \[[\s\S]*?\n {12}ICON_CHUNK,\n/); + }); +}); diff --git a/client/vite.config.js b/client/vite.config.js index af4661cbc..ff771d48e 100644 --- a/client/vite.config.js +++ b/client/vite.config.js @@ -4,6 +4,7 @@ import { VitePWA } from 'vite-plugin-pwa'; import { visualizer } from 'rollup-plugin-visualizer'; import { rtlTextAlias, plyrSpriteAlias } from './rtlTextAlias.js'; import { withoutRuntimeFonts } from './bookFontPrecache.js'; +import { ICON_CHUNK } from './iconChunk.js'; import { readFileSync } from 'node:fs'; // The version this bundle is built as, baked in at build time. The Docker build @@ -70,7 +71,8 @@ export default defineConfig(({ mode }) => ({ // offline-first travel planner a route the user never opened before losing // signal still has to work. The trade is that splitting buys first paint and // not install size: 107 entries / 17,795 KiB before any of it, 463 / - // 23,292 KiB now (measured, not estimated). + // 23,292 KiB after it, and 460 / 59,426 KiB as of 5.1.2 with the icons in + // one chunk (iconChunk.js; 652 entries without it). All measured, not estimated. // // Keep this figure honest. #2228 traced PWA boot failures to the browser // evicting this origin's whole bucket, precached shell included, and this @@ -350,6 +352,7 @@ export default defineConfig(({ mode }) => ({ tags: ['$initial'], test: /[\\/]node_modules[\\/](zustand|dexie|axios|zod|dompurify|isomorphic-dompurify)[\\/]/, }, + ICON_CHUNK, ], }, },