Turbopack: expose `unstable_splitCssPerRule` or default it off in dev (one module per CSS rule makes `next dev` memory-bound)
Suggested fix
Make per-rule CSS splitting conditional on the environment in @vanilla-extract/turbopack-plugin:
root: loaderContext.rootContext,
identifiers,
enableFileWatcher: false,
- unstable_splitCssPerRule: true,
+ unstable_splitCssPerRule: process.env.NODE_ENV === 'production',(or expose the flag through next-plugin's unstable_turbopack options so consumers can set it).
This is the same NODE_ENV check getOrMakeCompiler already uses two lines earlier to pick identifiers. I've been running exactly this as a pnpm patch — measurements below.
Why
The loader hardcodes unstable_splitCssPerRule: true, which creates roughly one virtual Turbopack module per CSS rule. For a sprinkles-based design system that is enormous, and [email protected] forwards only nextEnv, outputCss and identifiers, so it can't be tuned from userland. The flag is true in every published version (0.1.1 → 0.1.4).
Driving @vanilla-extract/[email protected] directly over two files from our design system (sprinkles.css.ts + theme.css.ts), counting cssImportSpecifier calls:
unstable_splitCssPerRule |
virtual CSS modules | CSS emitted |
|---|---|---|
true |
8,676 | 1,347,881 B |
false |
3 | 1,356,554 B |
Byte-for-byte the same CSS, ~2,900x the modules.
Impact in dev
Real app: [email protected], Pages Router, pnpm monorepo, 43 .css.ts files, ~10.8k atomic rules. Cold .next, one request to one page:
| cold compile | dev server peak footprint | |
|---|---|---|
| unpatched | 189 s | 22 GB |
| patched (dev splitting off) | 51 s | 3.4 GB |
22 GB exhausted a 32 GB machine and pushed it into swap. It is not a leak — footprint is flat across repeated recompiles (6 identical rebuilds, +0.00 GB each). It's a one-time watermark from graph size.
Incidentally, unpatched dev also served two ~4.1 MB CSS chunks that were near-identical; patched serves 2.38 MB total.
Minimal reproduction
https://github.com/AlexHartford/ve-turbopack-dev-memory-repro
npm install
VE_SCALE=1200 npm run count-modules # 10,802 modules vs 1, same CSS
VE_SCALE=1200 npm run measure # next dev cold compile + peak footprintVE_SCALE scales the sprinkles config; rules ≈ 9 × VE_SCALE + 2.
VE_SCALE |
modules | cold compile | peak footprint |
|---|---|---|---|
| 40 | 362 | 3.1 s | 644 MB |
| 400 | 3,602 | 7.7 s | 1,328 MB |
| 1,200 | 10,802 | 19.8 s | 2,153 MB |
The minimal repro shows the scaling trend rather than the full 22 GB — that needs a real app's graph, where the CSS modules compound with everything else.
Production is unaffected by the fix
Per-rule splitting is what lets production tree-shake unused rules, so gating on NODE_ENV keeps it. Verified on an App Router app in the same monorepo: next build emits a byte-identical 573,706 bytes of CSS with the same selector set, patched vs unpatched (cmp clean).
Measurement note
Use physical footprint (vmmap --summary <pid> / footprint -p <pid>), not RSS. Under memory pressure macOS compresses pages, so RSS falls while real usage grows — ps reported ~6 GB for a process whose actual footprint was 19 GB with CMPRS 19G. RSS hides this entirely, which cost me a while.
Versions
[email protected], @vanilla-extract/[email protected], @vanilla-extract/[email protected], @vanilla-extract/[email protected], @vanilla-extract/[email protected], @vanilla-extract/[email protected], pnpm 10.32.0, macOS arm64, 32 GB.
Possibly related: vercel/next.js#97802 (Turbopack saturates 4 GB with a 12k-module graph), vercel/next.js#98071.
Happy to open a PR with the one-line change if that's welcome.
Source: vanilla-extract-css/vanilla-extract