perf(build): granular code-split + preconnect hint for cold-load speed

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 `<link rel=preconnect>` 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.
This commit is contained in:
medievalshell
2026-05-21 02:03:38 +02:00
parent 9e38de6160
commit 0c7814fe04
2 changed files with 37 additions and 6 deletions
+3
View File
@@ -4,6 +4,9 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>Nitro</title>
<!-- Connection hints for the two domains the loader hits first.
dns-prefetch + preconnect halve the TLS round-trips on cold load. -->
<link rel="preconnect" href="https://challenges.cloudflare.com" crossorigin />
<script async defer src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit"></script>
</head>
<body>
+34 -6
View File
@@ -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';
}
}