Describe the bug
With a nested dynamic import of the form import('./a').then(() => import('./b')) where ./a has any CSS side-effect dep, the outer dynamic import in the built entry chunk is wrapped as __vitePreload(() => import('./a-…js'), void 0) instead of __vitePreload(..., __vite__mapDeps([…])). The inner import's deps array is correct.
The CSS file is emitted to disk and even appears in the __vite__mapDeps lookup table, but nothing references its index — no preload call, no <link> in the generated HTML. The stylesheet never loads in production. Dev mode (vite) works fine because the dev server injects CSS via JS.
Expected
import('./a.js') should be wrapped as __vitePreload(() => import('./a-…js'), __vite__mapDeps([…, 'a-….css'])) so the CSS dep is preloaded as a <link rel="stylesheet"> when the chunk loads.
Actual
The built entry chunk ends with:
js r(()=>import(`./a-….js`).then(e=>{...,r(()=>import(`./b-….js`),[])}), void 0); // ↑ outer call: deps lost
__vite__mapDeps table contains both a-….js and a-….css, but no code path references them.
Root cause
packages/vite/src/node/plugins/importAnalysisBuild.ts, generateBundle hook, walks dynamic import() calls in source-text order and pairs each with the next __VITE_PRELOAD__ marker after its end position.
After the transform pass, the chunk source has the shape:
js __vitePreload(() => import('./a'), __VITE_PRELOAD__ /* B */).then(() => { __vitePreload(() => import('./b'), __VITE_PRELOAD__ /* A */); });
Inner marker A textually precedes outer marker B because it sits inside the .then(...) callback. The pairing loop walks imports in order [a, b]:
./a finds marker A as the next marker after end_a → writes ./a's deps to A; adds posA to rewroteMarkerStartPos.
./b finds marker A again as the next marker after end_b → overwrites A with ./b's deps.
- Fallback pass finds marker B not in
rewroteMarkerStartPos → replaces with "void 0".
./a's preload deps (which would have included its CSS) are silently lost.
Suggested fix
Skip past markers already claimed by a previous import in the same iteration:
ts let markerStartPos = findPreloadMarker(code, end); while (markerStartPos !== -1 && rewroteMarkerStartPos.has(markerStartPos)) { markerStartPos = findPreloadMarker(code, markerStartPos + preloadMarker.length); }
Or, more robustly, pair markers to imports by AST relationship rather than source-text proximity.
Why this only surfaces on Vite 8
The latent bug exists in Vite 7 too. With Rollup, CSS attached to nested chunks was hoisted to the nearest entry/parent chunk, so the CSS dep ended up on a chunk whose dynamic import wasn't trapped in a nested-then. Vite 8 ships Rolldown, which keeps CSS on the original chunk — exposing the bug. Same chunk-shape change documented in laravel/framework#59662.
Related but distinct
Reproduction
https://github.com/charlsantony/vite-css-preload-bug
Steps to reproduce
- Clone the repro repo
npm install
npm run build
- Open
dist/assets/index-*.js — the trailing __vitePreload call has void 0 as its second argument
grep -rn "a-.*\.css" dist/ — the CSS filename only appears inside the __vite__mapDeps string table, never in index.html or any preload call
npm run preview — body background should be hotpink, renders white
Bug is build-only. vite (dev mode) is unaffected.
System Info
System:
OS: macOS 26.5
CPU: (12) arm64 Apple M3 Pro
Shell: zsh
Binaries:
Node: 22.20.0
npm: 10.9.3
npmPackages:
vite: 8.0.16 => 8.0.16
Used Package Manager
pnpm
Logs
No response
Validations
Describe the bug
With a nested dynamic import of the form
import('./a').then(() => import('./b'))where./ahas any CSS side-effect dep, the outer dynamic import in the built entry chunk is wrapped as__vitePreload(() => import('./a-…js'), void 0)instead of__vitePreload(..., __vite__mapDeps([…])). The inner import's deps array is correct.The CSS file is emitted to disk and even appears in the
__vite__mapDepslookup table, but nothing references its index — no preload call, no<link>in the generated HTML. The stylesheet never loads in production. Dev mode (vite) works fine because the dev server injects CSS via JS.Expected
import('./a.js')should be wrapped as__vitePreload(() => import('./a-…js'), __vite__mapDeps([…, 'a-….css']))so the CSS dep is preloaded as a<link rel="stylesheet">when the chunk loads.Actual
The built entry chunk ends with:
js r(()=>import(`./a-….js`).then(e=>{...,r(()=>import(`./b-….js`),[])}), void 0); // ↑ outer call: deps lost __vite__mapDepstable contains botha-….jsanda-….css, but no code path references them.Root cause
packages/vite/src/node/plugins/importAnalysisBuild.ts,generateBundlehook, walks dynamicimport()calls in source-text order and pairs each with the next__VITE_PRELOAD__marker after itsendposition.After the transform pass, the chunk source has the shape:
js __vitePreload(() => import('./a'), __VITE_PRELOAD__ /* B */).then(() => { __vitePreload(() => import('./b'), __VITE_PRELOAD__ /* A */); }); Inner marker A textually precedes outer marker B because it sits inside the
.then(...)callback. The pairing loop walks imports in order[a, b]:./afinds marker A as the next marker afterend_a→ writes./a's deps to A; adds posA torewroteMarkerStartPos../bfinds marker A again as the next marker afterend_b→ overwrites A with./b's deps.rewroteMarkerStartPos→ replaces with"void 0"../a's preload deps (which would have included its CSS) are silently lost.Suggested fix
Skip past markers already claimed by a previous import in the same iteration:
ts let markerStartPos = findPreloadMarker(code, end); while (markerStartPos !== -1 && rewroteMarkerStartPos.has(markerStartPos)) { markerStartPos = findPreloadMarker(code, markerStartPos + preloadMarker.length); } Or, more robustly, pair markers to imports by AST relationship rather than source-text proximity.
Why this only surfaces on Vite 8
The latent bug exists in Vite 7 too. With Rollup, CSS attached to nested chunks was hoisted to the nearest entry/parent chunk, so the CSS dep ended up on a chunk whose dynamic import wasn't trapped in a nested-then. Vite 8 ships Rolldown, which keeps CSS on the original chunk — exposing the bug. Same chunk-shape change documented in laravel/framework#59662.
Related but distinct
./aas a real JS chunk (it has a real export consumed bymain.js) to avoid that path.Reproduction
https://github.com/charlsantony/vite-css-preload-bug
Steps to reproduce
npm installnpm run builddist/assets/index-*.js— the trailing__vitePreloadcall hasvoid 0as its second argumentgrep -rn "a-.*\.css" dist/— the CSS filename only appears inside the__vite__mapDepsstring table, never inindex.htmlor any preload callnpm run preview— body background should be hotpink, renders whiteBug is build-only.
vite(dev mode) is unaffected.System Info
System: OS: macOS 26.5 CPU: (12) arm64 Apple M3 Pro Shell: zsh Binaries: Node: 22.20.0 npm: 10.9.3 npmPackages: vite: 8.0.16 => 8.0.16Used Package Manager
pnpm
Logs
No response
Validations