Skip to content

Statically imported chunk can lose its modulepreload and CSS when another import reaches it dynamically first #17346

Description

@xyrolle

Describe the bug

When the build walks a page's chunk graph and reaches a shared chunk through a dynamic import() before it reaches the same chunk through a static import, the chunk, and any of its static dependencies that were not already reached statically, can be left out of the page's preload list, and their CSS can be left out of the server-rendered HTML.

In the reproduction, the root page statically imports a.js and b.js. a.js dynamically imports c.js, and b.js statically imports c.js, which imports c.css. c.js runs when the page loads (b() calls it during render), but after vite build:

  • the root page's server node has no entry for the c chunk in imports, so the HTML for / has no <link rel="modulepreload"> for it, and
  • the node's stylesheets is empty, so the HTML for / has no <link rel="stylesheet"> for c.css. With JavaScript disabled the paragraph stays unstyled. With JavaScript enabled the stylesheet is only added later, by Vite's preload helper when the client imports node 2, so the c chunk and c.css are fetched in a second round-trip.

The /b page, which reaches c.js only statically, gets both.

I'd like to submit a PR for this.

Reproduction

https://github.com/xyrolle/sveltekit-find-deps-repro

npm install && npx vite build, then look at .svelte-kit/output/server/nodes/2.js (the / page), or run npx vite preview and view the source of /.

Expected: node 2's imports includes the c chunk and its stylesheets includes c.css, and the HTML for / has a modulepreload link for the c chunk and a stylesheet link for c.css.

Actual: both are missing (output below).

The reproduction's files

package.json:

{
	"name": "find-deps-repro",
	"private": true,
	"type": "module",
	"scripts": { "build": "vite build", "preview": "vite preview" },
	"devDependencies": {
		"@sveltejs/kit": "3.0.0",
		"@sveltejs/vite-plugin-svelte": "7.3.1",
		"svelte": "5.57.1",
		"vite": "8.3.2"
	}
}

vite.config.js:

import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';

export default defineConfig({
	plugins: [sveltekit({ adapter: { name: 'noop', adapt() {} } })]
});

src/app.html:

<!doctype html>
<html><head>%sveltekit.head%</head><body>%sveltekit.body%</body></html>

src/lib/a.js, src/lib/b.js, src/lib/c.js, src/lib/c.css:

// src/lib/a.js
export const load_c = () => import('./c.js');

// src/lib/b.js
import { c } from './c.js';

export const b = () => c();

// src/lib/c.js
import './c.css';

export const c = () => 'styled by c.css';
/* src/lib/c.css */
.c { color: red; }

src/routes/+page.svelte:

<script>
	import { load_c } from '../lib/a.js';
	import { b } from '../lib/b.js';
</script>

<p class="c">{b()}</p>
<button onclick={load_c}>load c</button>

src/routes/a/+page.svelte and src/routes/b/+page.svelte only exist so that a.js and b.js end up in separate chunks:

<script>
	import { load_c } from '../../lib/a.js';
</script>

<button onclick={load_c}>load c</button>
<script>
	import { b } from '../../lib/b.js';
</script>

<p>{b()}</p>

Logs

# .svelte-kit/output/client/.vite/manifest.json (relevant entries)
_aT2b8Kyi.js (a)  imports: ["_BaNbYf_w.js"]  dynamicImports: ["_BdQ6-hIb.js"]
_44AlM8TR.js (b)  imports: ["_BdQ6-hIb.js"]
_BdQ6-hIb.js (c)  css: ["_app/immutable/assets/c.BJ3smcyB.css"]

# .svelte-kit/output/server/nodes/2.js
export const imports = ["_app/immutable/nodes/2.D-v6z5Iy.js","_app/immutable/chunks/Dym8azR3.js","_app/immutable/chunks/7_WjaUPM.js","_app/immutable/chunks/aT2b8Kyi.js","_app/immutable/chunks/BaNbYf_w.js","_app/immutable/chunks/44AlM8TR.js"];
export const stylesheets = [];

System Info

System:
  OS: macOS 27.2
  CPU: (18) arm64 Apple M5 Pro
Binaries:
  Node: 24.16.0
  npm: 11.13.0
npmPackages:
  @sveltejs/kit: 3.0.0 => 3.0.0
  @sveltejs/vite-plugin-svelte: 7.3.1 => 7.3.1
  svelte: 5.57.1 => 5.57.1
  vite: 8.3.2 => 8.3.2

Severity

serious, but I can work around it

Additional Information

The cause is in find_deps in packages/kit/src/exports/vite/build/utils.js. For page nodes it is called with add_dynamic_css = true, so it walks the manifest depth-first. It starts with add_js = true, keeps the current value when it follows imports, and passes add_js = false when it follows dynamicImports. Both walks share one seen set. In the reproduction, the walk reaches a, then follows a's dynamicImports into c with add_js = false, which marks c as seen without adding it to imports. When the walk then reaches c again through b, c is already in seen and is skipped, so c is not added to imports, and neither is anything it statically imports that wasn't already reached statically. For the same reason, c's CSS is grouped in stylesheet_map under c (the dynamic import) and not under the page, so in the reproduction it does not end up in the page node's stylesheets. The same code is on main.

Whether a chunk is affected depends on the order in which the walk meets its importers, and nothing warns when it happens. In our app, which groups chunks manually, one route preloaded 25 of the 203 chunks in its static closure, and simulated LCP went from 1.25 s to 2.5 s until we patched find_deps locally.

Activity

  1. svelte-triage-bot commented on Oct 4, 2026

    @svelte-triage-bot
    Contributor

    I reproduced the issue, but could not produce a safe, verified fix.

    What I tried

    Why I stopped
    Open pull request #17347 already implements and tests a fix for #17346. Per the required workflow, duplicate implementation work must stop and be reported as too_hard, naming the existing PR.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions