From 49e68fb07c1fb4ac282be7c5688e455ec96299f3 Mon Sep 17 00:00:00 2001 From: Maurice <61554723+mauriceboe@users.noreply.github.com> Date: Fri, 7 Aug 2026 12:01:43 +0200 Subject: [PATCH] build(client): give react and the core libs their own chunks (#1818) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Everything that changes with a release currently drags the entry chunk with it, and 63 of 141 chunks import the entry statically. Measured with a one-token edit to a leaf route module and a rebuild, 70 of 141 chunks rehash — just over 1 MB gzip that every installed PWA re-downloads per deploy, since the service worker registers with autoUpdate. Pulling react and the core libraries out cuts that to 847 kB: rehashed 70/141 -> 60/140 gzip 1,038,497 B -> 847,688 B (-18.4%) entry 580,672 B -> 258,105 B eager JS 1,482,722 B -> 1,482,523 B (unchanged) eager CSS same file, same hash No first paint win, then — the gain is a smaller update delta and a halved entry to parse on weak devices. Three of the five groups the plan called for are left out, and not silently: mapbox-gl/maplibre-gl, leaflet and react-markdown already sit in async chunks whose hashes survive a release, so a group would only rename them. Worse, without tags: ['$initial'] a group also collects modules that hang behind React.lazy today and turns its chunk into a static import of the entry — measured, that put the 2.8 MB GL chunk into the index.html modulepreload and took eager JS to 4.61 MB, undoing the route split for maps. Note for the next person to touch this: vite 8 bundles with rolldown, so rollupOptions is only an alias and both manualChunks and advancedChunks are deprecated in favour of codeSplitting.groups. A config using the old names builds green and has no effect. Also refreshes two comments that still quoted pre-split bundle numbers. --- client/vite.config.js | 52 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/client/vite.config.js b/client/vite.config.js index c311e4118..2b4ad8570 100644 --- a/client/vite.config.js +++ b/client/vite.config.js @@ -27,14 +27,14 @@ export default defineConfig(({ mode }) => ({ // Anything above this is dropped from the precache manifest. The build does // not fail over it, it only prints "won't be precached", so the ceiling has // to sit close to the real bundle or an accidental heavyweight goes - // offline-broken unnoticed. Largest precached entry after route splitting is - // the heic-to chunk at 3.0 MB; the entry chunk is down to 1.2 MB. + // offline-broken unnoticed. Largest precached entry is the heic-to chunk at + // 3.0 MB, which leaves about 670 kB of headroom; the entry chunk is 258 kB. maximumFileSizeToCacheInBytes: 3.5 * 1024 * 1024, // Every route chunk is precached alongside the shell, deliberately: for an // offline-first travel planner a route the user never opened before losing - // signal still has to work. The trade is that route splitting buys first - // paint and not install size — 107 entries / 17,795 KiB before the split, - // 178 / 17,826 KiB after. + // 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, 220 / + // 17,855 KiB now. globPatterns: ['**/*.{js,css,html,svg,png,woff,woff2,ttf}'], // build:analyze drops a treemap next to the app; it must never end up in a // precache manifest if someone ships that build by accident. @@ -153,6 +153,48 @@ export default defineConfig(({ mode }) => ({ target: 'es2022', sourcemap: false, modulePreload: { polyfill: true }, + // Vite 8 bundles with rolldown, not rollup. `rollupOptions` is only an alias + // onto `rolldownOptions`, and both `manualChunks` and `advancedChunks` are + // deprecated in favour of `codeSplitting.groups` — a config mixing them still + // builds green and simply has no effect. + // + // `tags: ['$initial']` is not optional here. Without it a group also collects + // modules that today hang behind React.lazy, which turns the group chunk into + // a static import of the entry: measured, that put the 2.8 MB GL chunk into + // the index.html modulepreload and took eager JS from 1.48 MB to 4.61 MB. + // + // Deliberately no groups for mapbox-gl/maplibre-gl, leaflet or react-markdown. + // Those already sit in async chunks of their own with hashes that survive a + // release; a group would only rename them, and at worst make them eager. + rolldownOptions: { + output: { + codeSplitting: { + groups: [ + { + name: 'vendor-react', + priority: 40, + tags: ['$initial'], + // react-dom/server is pulled dynamically by TripPDF and lives in an + // async chunk. Excluding it keeps a later static import from lifting + // ~170 kB of Fizz into the eager vendor chunk. + test: (id) => + /[\\/]node_modules[\\/](react|react-dom|react-router|scheduler)[\\/]/.test(id) && + !/react-dom[\\/](server|static)|react-dom-server/.test(id), + }, + { + name: 'vendor-core', + priority: 30, + // zod and dompurify arrive through @trek/shared but live in + // node_modules themselves, so they are caught here. @trek/shared is + // not: it resolves to shared/dist without a node_modules segment, + // and its contract code changes with every release anyway. + tags: ['$initial'], + test: /[\\/]node_modules[\\/](zustand|dexie|axios|zod|dompurify|isomorphic-dompurify)[\\/]/, + }, + ], + }, + }, + }, }, server: { port: 5173,