Describe the bug
Describe the bug
vite:build-html's generateBundle decides whether the HTML entry chunk can be inlined by running a single anchored regex over the entire minified entry chunk:
https://github.com/vitejs/vite/blob/main/packages/vite/src/node/plugins/html.ts
const entirelyImportRE =
/^(?:import\s*(?:"[^"\n]*[^\\\n]"|'[^'\n]*[^\\\n]');*|\/\*[\s\S]*?\*\/|\/\/.*[$\n])*$/
When an app imports many pure-CSS JS modules that end up as separate chunks (e.g. a multi-theme app importing one scope.css per theme), CSS extraction leaves those chunks empty and each import statement in the entry chunk is replaced with a /* empty css */ marker. The minified entry chunk then starts with N consecutive comment blocks followed by real code.
Each comment boundary is a backtracking choice point for the lazy \/\*[\s\S]*?\*\/ alternative inside (?: ... )* (one "comment" match can lazily extend across any number of following blocks). Since the overall match always fails (the chunk contains real code), the regex engine explores ~2^(N-1) paths before returning false. Build time grows ×4 for every +2 markers:
| N pure-CSS chunks |
vite build (vite 8.1.3) |
| 15 |
0.9s |
| 20 |
1.0s |
| 24 |
4.0s |
| 26 |
13.1s |
| 28 |
50.3s |
| 30 |
197.9s |
A --cpu-prof of the N=26 build attributes 96.9% of all samples to this regex. In the real-world app where I found this (27 Bootswatch themes → 27 markers, followed by a 200KB one-line chunk whose // occurrences add further choice points), vite build sat inside this single regex.test() call for 3.5 hours, with the process not even responding to SIGTERM (the JS thread never yields). Attaching the inspector to the stuck process showed 39,832 of 39,833 CPU samples inside isEntirelyImport.
Expected behavior: the "is this chunk entirely imports?" check should be linear in chunk size, and answer false quickly for a chunk that contains real code.
Standalone one-liner showing the explosion without Vite:
const re = /^(?:import\s*(?:"[^"\n]*[^\\\n]"|'[^'\n]*[^\\\n]');*|\/\*[\s\S]*?\*\/|\/\/.*[$\n])*$/
re.test('/* empty css */'.repeat(30) + 'const x=1;') // ~30s; ×4 per +2 repeats
Notes:
{
name: 'defuse-empty-css-regex-bomb',
apply: 'build',
generateBundle(_options, bundle) {
for (const chunk of Object.values(bundle)) {
if (chunk.type === 'chunk' && chunk.isEntry && !chunk.code.startsWith(';')) {
chunk.code = `;${chunk.code}`
}
}
},
}
- Suggested fix: replace
entirelyImportRE.test() with a small linear scanner (skip whitespace/comments/import "…"; tokens iteratively), or at least eliminate the ambiguity between the comment alternatives and the outer repetition.
Reproduction
https://stackblitz.com/edit/vitejs-vite-2dzw3zev?file=README.md
Steps to reproduce
Happens during build only (vite build).
npm install
npm run build — src/ is pre-generated with N=28 pure-CSS chunks → ~50s inside the regex (baseline N=15 build is <1s)
- Optionally scale N to see the exponential curve:
node make-repro.mjs 30 && npm run build → ~200s; N=40 extrapolates to hours.
The repro is a vanilla project (no framework, no plugins): main.js statically imports N one-line JS modules that each import a tiny CSS file; manualChunks gives each its own chunk so CSS extraction empties them and the entry chunk starts with N /* empty css */ markers.
System Info
System:
OS: Linux 6.18 Ubuntu 24.04.4 LTS 24.04.4 LTS (Noble Numbat)
CPU: (4) x64 Intel(R) Xeon(R) Processor @ 2.80GHz
Memory: 15.11 GB / 15.70 GB
Container: Yes
Shell: 5.2.21 - /bin/bash
Binaries:
Node: 22.22.2 - /opt/node22/bin/node
npm: 10.9.7 - /opt/node22/bin/npm
npmPackages:
vite: ^8.1.3 => 8.1.3
Used Package Manager
npm
Logs
No response
Validations
Describe the bug
Describe the bug
vite:build-html'sgenerateBundledecides whether the HTML entry chunk can be inlined by running a single anchored regex over the entire minified entry chunk:https://github.com/vitejs/vite/blob/main/packages/vite/src/node/plugins/html.ts
When an app imports many pure-CSS JS modules that end up as separate chunks (e.g. a multi-theme app importing one
scope.cssper theme), CSS extraction leaves those chunks empty and each import statement in the entry chunk is replaced with a/* empty css */marker. The minified entry chunk then starts with N consecutive comment blocks followed by real code.Each comment boundary is a backtracking choice point for the lazy
\/\*[\s\S]*?\*\/alternative inside(?: ... )*(one "comment" match can lazily extend across any number of following blocks). Since the overall match always fails (the chunk contains real code), the regex engine explores ~2^(N-1) paths before returningfalse. Build time grows ×4 for every +2 markers:vite build(vite 8.1.3)A
--cpu-profof the N=26 build attributes 96.9% of all samples to this regex. In the real-world app where I found this (27 Bootswatch themes → 27 markers, followed by a 200KB one-line chunk whose//occurrences add further choice points),vite buildsat inside this singleregex.test()call for 3.5 hours, with the process not even responding to SIGTERM (the JS thread never yields). Attaching the inspector to the stuck process showed 39,832 of 39,833 CPU samples insideisEntirelyImport.Expected behavior: the "is this chunk entirely imports?" check should be linear in chunk size, and answer
falsequickly for a chunk that contains real code.Standalone one-liner showing the explosion without Vite:
Notes:
vite:build-htmlruns its check, not the regex itself (which is equally vulnerable in both).vite buildterribly slow #10900 / Library mode umd build hangs #14065 (catastrophic backtracking in the UMD helper regex, fixed by rewriting the check), fix: use precise regexes for transform filter to avoid backtracking #21800.;to entry chunks ingenerateBundle(user plugins run beforevite:build-html), which makes the anchored regex fail at the first alternative in O(1) with the same verdict:entirelyImportRE.test()with a small linear scanner (skip whitespace/comments/import "…";tokens iteratively), or at least eliminate the ambiguity between the comment alternatives and the outer repetition.Reproduction
https://stackblitz.com/edit/vitejs-vite-2dzw3zev?file=README.md
Steps to reproduce
Happens during build only (
vite build).npm installnpm run build—src/is pre-generated with N=28 pure-CSS chunks → ~50s inside the regex (baseline N=15 build is <1s)node make-repro.mjs 30 && npm run build→ ~200s; N=40 extrapolates to hours.The repro is a vanilla project (no framework, no plugins):
main.jsstatically imports N one-line JS modules that each import a tiny CSS file;manualChunksgives each its own chunk so CSS extraction empties them and the entry chunk starts with N/* empty css */markers.System Info
System: OS: Linux 6.18 Ubuntu 24.04.4 LTS 24.04.4 LTS (Noble Numbat) CPU: (4) x64 Intel(R) Xeon(R) Processor @ 2.80GHz Memory: 15.11 GB / 15.70 GB Container: Yes Shell: 5.2.21 - /bin/bash Binaries: Node: 22.22.2 - /opt/node22/bin/node npm: 10.9.7 - /opt/node22/bin/npm npmPackages: vite: ^8.1.3 => 8.1.3Used Package Manager
npm
Logs
No response
Validations