From 0c7814fe0480925cffd093133458ff01810af75e Mon Sep 17 00:00:00 2001 From: medievalshell Date: Thu, 21 May 2026 02:03:38 +0200 Subject: [PATCH] perf(build): granular code-split + preconnect hint for cold-load speed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The vendor chunk was a single ~1MB blob (react + tanstack-query + framer-motion + jodit + emoji-mart + react-icons + howler + zustand + json5 all merged), forcing every cold load to wait on the slowest of those modules before the page could interactivate. Split it into domain-specific chunks so HTTP/2 multiplexing can pull them in parallel and CF can cache each independently: - vendor-pixi (pixi.js + pixi-filters — when rollup actually splits; currently inlined into the umbrella renderer chunk because nitro-renderer is its sole importer) - vendor-audio (howler) - vendor-emoji (@emoji-mart — heaviest at ~430KB, only used in chat so a longer-term win is making it lazy) - vendor-editor (jodit + @react-page — admin-only news editor) - vendor-react (react / react-dom / scheduler / error-boundary) - vendor-motion / vendor-query / vendor-icons / vendor-state / vendor-json5 - nitro-renderer-{avatar,communication,room,assets} — heaviest renderer packages get their own chunks when imported directly (the umbrella @nitrots/nitro-renderer still hosts the rest) Also add a `` for challenges.cloudflare.com so the Turnstile JS handshake doesn't pay an extra TLS round-trip on the first paint. Net effect: roughly the same total bytes shipped on a cold load, but they fetch in parallel instead of sequentially, and a warm second visitor only re-downloads the chunks whose code actually changed. --- index.html | 3 +++ vite.config.mjs | 40 ++++++++++++++++++++++++++++++++++------ 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/index.html b/index.html index 48e1e0a..b162833 100644 --- a/index.html +++ b/index.html @@ -4,6 +4,9 @@ Nitro + + diff --git a/vite.config.mjs b/vite.config.mjs index 1b6899f..e51a1a0 100644 --- a/vite.config.mjs +++ b/vite.config.mjs @@ -164,18 +164,46 @@ export default defineConfig({ rollupOptions: { output: { assetFileNames: 'src/assets/[name]-[hash].[ext]', + // Granular chunking: split the monolithic vendor / nitro-renderer + // bundles into smaller chunks so the browser can fetch them in + // parallel and CF can cache each independently. Splits chosen + // by size impact (pixi ~600KB, react ~150KB, framer-motion ~100KB, + // jodit ~250KB lazy-loaded only by admin news, etc.). manualChunks: id => { - // Renderer source is consumed via filesystem alias - // (../Nitro_Render_V3/packages/*/src) so it is NOT - // under node_modules — needs its own branch before - // the node_modules check. - if(id.includes('Nitro_Render_V3') || id.includes(`${ rendererRoot }`)) return 'nitro-renderer'; + // Vendor checks first — pixi.js/howler are aliased to + // ../Nitro_Render_V3/node_modules so they match + // `Nitro_Render_V3` too. Without this priority, they end + // up bundled into nitro-renderer instead of getting their + // own chunks (pixi alone is ~600KB). Use `/pixi.js/` to + // avoid matching path fragments like `assets/pixi.js/`. + const norm = id.replace(/\\/g, '/'); + if(norm.includes('pixi.js') || norm.includes('pixi-filters')) return 'vendor-pixi'; + if(norm.includes('howler')) return 'vendor-audio'; + if(norm.includes('@emoji-mart')) return 'vendor-emoji'; + if(norm.includes('jodit') || norm.includes('@react-page')) return 'vendor-editor'; + + if(id.includes('Nitro_Render_V3') || id.includes(`${ rendererRoot }`)) + { + // Heaviest renderer packages get their own chunks so + // pages that don't touch them (login flow, very early + // boot) don't have to pay for them upfront. + if(id.includes('/packages/avatar/')) return 'nitro-renderer-avatar'; + if(id.includes('/packages/communication/')) return 'nitro-renderer-comm'; + if(id.includes('/packages/room/')) return 'nitro-renderer-room'; + if(id.includes('/packages/assets/')) return 'nitro-renderer-assets'; + return 'nitro-renderer'; + } if(id.includes('node_modules')) { if(id.includes('@nitrots/nitro-renderer') || id.includes('renderer3')) return 'nitro-renderer'; - + if(id.match(/\/react(-dom)?\/|\/scheduler\//) || id.includes('react-error-boundary')) return 'vendor-react'; + if(id.includes('framer-motion')) return 'vendor-motion'; + if(id.includes('@tanstack')) return 'vendor-query'; + if(id.includes('zustand') || id.includes('use-between')) return 'vendor-state'; + if(id.includes('react-icons')) return 'vendor-icons'; + if(id.includes('json5')) return 'vendor-json5'; return 'vendor'; } }