Skip to content

fix: preload css for nested dynamic imports (fix #22700)#22721

Open
mturac wants to merge 2 commits into
vitejs:mainfrom
mturac:fix/issue-22700-preload-marker-pairing
Open

fix: preload css for nested dynamic imports (fix #22700)#22721
mturac wants to merge 2 commits into
vitejs:mainfrom
mturac:fix/issue-22700-preload-marker-pairing

Conversation

@mturac

@mturac mturac commented Jun 19, 2026

Copy link
Copy Markdown

Description

Fixes #22700.

Nested dynamic imports could reuse a preload marker that had already been paired with an earlier import. When that happened, the outer import could lose its dependency list and the CSS for the imported chunk would not be loaded in production builds.

This change skips already-consumed preload markers before rewriting the marker for the current dynamic import.

The regression test exercises the build-only behavior in the dynamic-import playground by checking that the nested import creates a stylesheet link and applies the imported CSS.

Tests

  • pnpm run build
  • pnpm --filter vite run build
  • pnpm run test-build dynamic-import -t "should preload css for nested dynamic imports"
  • pnpm run test-build dynamic-import
  • pnpm exec eslint packages/vite/src/node/plugins/importAnalysisBuild.ts playground/dynamic-import/__tests__/dynamic-import.spec.ts playground/dynamic-import/nested/index.js playground/dynamic-import/nested/issue-22700-a.js playground/dynamic-import/nested/issue-22700-b.js
  • pnpm exec oxfmt --check packages/vite/src/node/plugins/importAnalysisBuild.ts playground/dynamic-import/__tests__/dynamic-import.spec.ts playground/dynamic-import/index.html playground/dynamic-import/nested/index.js playground/dynamic-import/nested/issue-22700-a.js playground/dynamic-import/nested/issue-22700-b.js playground/dynamic-import/nested/issue-22700-a.css
  • git diff --check

@shulaoda shulaoda self-assigned this Jun 23, 2026
@shulaoda

Copy link
Copy Markdown
Member

Thanks for digging into this. It does make the CSS load again, but the pairing it produces attaches each import's deps to the wrong marker.

For import('a').then(() => import('b')), Rolldown wraps the whole import().then(), so b's marker comes before a's in the text. Walking the imports from front to back, import('a') takes the first marker after it, which is actually b's, and then import('b') finds that marker already taken and falls through to the outer one. The two dep lists end up swapped:

__vitePreload(
  () =>
    import('a').then(() =>
      __vitePreload(() => import('b') /* a's deps */),
    ) /* b's deps */,
)

a's CSS is no longer dropped, which fixes the visible bug, but it now sits on the inner __vitePreload. That inner call only runs after a has loaded and its .then fires, so a's stylesheet is fetched late instead of alongside a.js, and each call's preload list points at the other chunk's assets. With three or more levels the dep lists rotate instead of swapping.

@mturac mturac force-pushed the fix/issue-22700-preload-marker-pairing branch from 72cd357 to ff84d70 Compare June 24, 2026 10:23
@mturac

mturac commented Jun 24, 2026

Copy link
Copy Markdown
Author

Thanks, that makes sense. I updated the branch to pair preload markers from the inside out by processing dynamic imports in reverse order.

I also tightened the build test so it checks the generated output shape: the outer import("./issue-22700-a") preload gets the dep map, while the inner import("./issue-22700-b") preload gets [].

Validated locally:

  • pnpm --filter vite build
  • VITE_TEST_BUILD=1 pnpm vitest run -c vitest.config.e2e.ts playground/dynamic-import/__tests__/dynamic-import.spec.ts -t "should preload css for nested dynamic imports"
  • VITE_TEST_BUILD=1 pnpm vitest run -c vitest.config.e2e.ts playground/dynamic-import/__tests__/dynamic-import.spec.ts
  • pnpm exec oxfmt --check packages/vite/src/node/plugins/importAnalysisBuild.ts playground/dynamic-import/__tests__/dynamic-import.spec.ts playground/dynamic-import/nested/index.js
  • pnpm exec eslint packages/vite/src/node/plugins/importAnalysisBuild.ts playground/dynamic-import/__tests__/dynamic-import.spec.ts
  • pnpm --filter vite typecheck

@shulaoda

Copy link
Copy Markdown
Member

Nice, the backwards + skip-claimed version pairs them correctly now. Each nested import gets its own deps, so #22700 is fixed and the swap is gone. I ran it against the original repro and a few nesting shapes, CSS lands on the right call.

Two tiny things, both minor:

  1. The backward loop re-scans the inner markers for every enclosing import (each findPreloadMarker walks past the already-claimed ones), so it's ~O(n²) on deep nesting. Doesn't matter for real code, just flagging it. A single forward pass with a stack scans once.

  2. Looping backwards also fills addFileDep in reverse, so __vite__mapDeps comes out reversed (the indices shift with it). Same at runtime, but the output changes, so it might move some snapshots.

Also tweaked the test a bit. The build regex pins the exact minified shape (the arrows and the \1 backref), so it'll break whenever the minifier output shifts. ,[] and __vite__mapDeps are injected after minify, so you can just check the inner import's slot:

const js = findAssetFile(/index-[-\w]{8}\.js$/) ?? ''
const innerPreload = js.match(
  /import\([^)]*issue-22700-b-[-\w]+\.js[^)]*\)[\s\S]*?(,\[\]\)|__vite__mapDeps)/,
)?.[1]
expect(innerPreload).toBe(',[])')

@shulaoda shulaoda left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. cc @sapphi-red, both this and #22759 fix it and this one's a smaller diff. Left a couple of small notes above.

@shulaoda shulaoda requested a review from sapphi-red June 24, 2026 23:55

@sapphi-red sapphi-red left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Marking so that it's clear that I'm reviewing this and #22759

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Build: nested import().then(() => import()) drops outer __vitePreload deps to void 0, orphaning CSS

3 participants