perf(desktop): split heavy lazy-only libs out of the renderer entry chunk

codeSplitting:false inlined every lazy()/dynamic import into the entry, so
the whole 28.5 MB bundle — including 19 MB of shiki grammars and 3.1 MB of
mermaid that nothing rendered — was parsed and evaluated on every cold
start. The original single-chunk rationale (#38888: electron-builder OOMs
scanning shiki's thousands of default chunks) doesn't require ONE chunk,
just few: rolldown advancedChunks groups keep the file count at ~180.

Ordering matters and is the subtle part: shared foundations (react, hast/
mdast utils, lodash-es/d3 commons) must match BEFORE the heavy groups,
because rolldown merges an unmatched shared module INTO the heavy chunk
that uses it — and then the entry statically imports 19 MB of shiki just
to reach react, putting the whole chunk back on the boot path.

Statically-reachable boot graph: 26.9 MB -> 7.7 MB.
This commit is contained in:
Brooklyn Nicholson 2026-07-27 20:17:38 -05:00
parent 71e7eb3c16
commit 6fb5d2d89c

View file

@ -53,16 +53,46 @@ export default defineConfig(({ command }) => ({
postcss: { plugins: [] }
},
build: {
// Keep desktop packaging stable: Shiki ships many dynamic chunks by
// default, and electron-builder can OOM scanning thousands of files.
// Collapsing to a single chunk is intentional, so the renderer bundle is
// large by design (~22 MB). Raise the warning ceiling above that so the
// cosmetic "chunk larger than 500 kB" nag stays quiet, while still acting
// as a regression alarm if the bundle balloons well past today's size.
// The renderer intentionally ships FEW chunks (not one, not thousands):
// · `codeSplitting: false` (the old setup) inlines every `lazy()` /
// dynamic import into the entry, so heavyweight lazy-only deps
// (mermaid, shiki grammars, katex) are parsed + evaluated on every
// cold start even though nothing rendered them. By the time the
// bundle hit ~28 MB that eval was ~1s of launch on an M-series.
// · Default splitting emits a chunk per shiki grammar/theme — thousands
// of files, which electron-builder OOMs scanning (#38888).
// `advancedChunks` is the middle ground: heavyweight libraries merge into
// a handful of named vendor chunks loaded on first use, app-level dynamic
// imports stay lazy, and the file count stays in the tens.
chunkSizeWarningLimit: 25000,
rolldownOptions: {
output: {
codeSplitting: false
advancedChunks: {
groups: [
// Shared foundations FIRST (first match wins): an unmatched
// module shared by the entry and a heavy chunk gets merged INTO
// the heavy chunk, and the entry then statically imports 19 MB of
// shiki just to reach react/hast utils — putting the heavy chunk
// right back on the boot path.
{ name: 'vendor-react', test: /node_modules[\\/](react|react-dom|scheduler)[\\/]/ },
{
name: 'vendor-md',
test: /node_modules[\\/](property-information|hast-util-[^\\/]+|mdast-util-[^\\/]+|micromark[^\\/]*|unist-util-[^\\/]+|vfile[^\\/]*|unified|stringify-entities|space-separated-tokens|comma-separated-tokens|zwitch|html-void-elements|devlop|style-to-js|style-to-object|clsx)[\\/]/
},
// Shared utility packages the entry ALSO uses — kept out of the
// heavy groups for the same boot-path reason.
{
name: 'vendor-util',
test: /node_modules[\\/](lodash-es|es-toolkit|uuid|dayjs|d3-array|d3-color|d3-force|d3-interpolate|d3-time[^\\/]*|dompurify|stylis)[\\/]/
},
// One chunk per heavyweight, lazy-only library family.
// @streamdown/code lives WITH shiki because it statically imports
// the full shiki bundle.
{ name: 'mermaid', test: /node_modules[\\/](mermaid|cytoscape|dagre|khroma|elkjs|d3|d3-[^\\/]+|@mermaid-js)[\\/]/ },
{ name: 'shiki', test: /node_modules[\\/](shiki|@shikijs|react-shiki|@streamdown[\\/]code|oniguruma-to-es|oniguruma-parser|regex(-[^\\/]+)?)[\\/]/ },
{ name: 'katex', test: /node_modules[\\/]katex[\\/]/ }
]
}
}
}
},