#98804·Next.js

Turbopack: nested next/dynamic gets no SSR stylesheet link, causing FOUC and layout shift (App Router)

Author: vlad-demchukCreated Sep 17, 2026Updated Sep 17, 2026
LabelsLazy LoadingTurbopackModule ResolutionCSS

Link to the code that reproduces this issue

https://github.com/vlad-demchuk/next16-nested-dynamic-css-repro

To Reproduce

npm install
npm run build     # Turbopack, the Next 16 default
npm run check

The app is six files:

app/page.js                      server component
  └─ components/Resolver.js      "use client", imported statically
       └─ dynamic(WidgetX)            level 1
            └─ dynamic(VariantY)      level 2

WidgetX and VariantY each import their own CSS module and both keep the default ssr: true. check.mjs reads the prerendered HTML, lists its <link rel="stylesheet"> tags, then locates the CSS chunk holding each module's class name and reports whether that chunk is linked.

To confirm it by hand rather than through the script:

npx next start -p 3200

Open http://localhost:3200 and use view-source — not the Elements panel, since by then hydration has already inserted the missing link. The server HTML contains exactly one <link rel="stylesheet">, the level-1 one. Throttling the network (DevTools -> Network -> Slow 3G) and hard-reloading makes the shift visible: the two columns stack full width first, then snap to 62% / 38% when the level-2 stylesheet arrives. On an unthrottled localhost the chunk is a few hundred bytes, so the flash is too quick to catch.

Current vs. Expected behavior

Current — the level-2 stylesheet is not in the server HTML at all. It is emitted as a chunk, but nothing links it, so the client chunk loader fetches it after hydration (plain <link>, no data-precedence, renderBlockingStatus: "non-blocking"). VariantY therefore paints without display: flex and without its 62% / 38% column widths, then reflows when the stylesheet lands.

Expected — the same <link rel="stylesheet" precedence="dynamic"> that the level-1 dynamic import receives in the very same build.

Results from the repro:

build level 1 CSS linked in SSR HTML level 2 CSS linked in SSR HTML
16.3.4, Turbopack yes (data-precedence="dynamic") no
16.3.4, next build --webpack yes yes
16.4.0-canary.34, Turbopack yes no
16.0.11, Turbopack yes yes

On one and the same version the webpack build links both stylesheets while the Turbopack build links only the first level, and the Turbopack build did link both on 16.0.x.

Provide environment information

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 25.5.0
  Available memory (MB): 32768
  Available CPU cores: 12
Binaries:
  Node: 24.19.0
  npm: 11.17.0
  pnpm: 10.20.0
Relevant Packages:
  next: 16.3.4 (also reproduced on 16.4.0-canary.34)
  react: 19.3.0
  react-dom: 19.3.0
Next.js Config:
  output: N/A

Which area(s) are affected? (Select all that apply)

Turbopack, CSS, Module Resolution, Lazy Loading (next/dynamic)

Which stage(s) are affected? (Select all that apply)

next build (local), next start (local), Vercel (Deployed)

Additional context

Regression range, measured on a real app (same application code, only the Next version changed). Metric = number of CSS modules on one route delivered by the client loader instead of an SSR link:

next late CSS modules
16.0.0 1
16.0.11 1
16.1.0 24
16.1.7 24
16.2.12 29
16.3.4 29
16.3.5 29

16.2.x also raised total emitted CSS for that route from 978 KB to 1.16 MB, which may or may not be the same change.

Impact in production on 16.3.4, on a landing page whose above-the-fold section is a level-2 dynamic: CLS 0.4157, from a single shift of 0.3825 at 1543 ms — 83 ms after first contentful paint, with the two columns and the widget wrapper as the shift sources. The statically imported equivalent component on the same page measures CLS 0.

The behaviour looks at odds with the intent stated in Next's own source, though we may be misreading how the pieces fit together:

  • packages/next/src/shared/lib/lazy-dynamic/loadable.tsx: /* During SSR, we need to preload the CSS from the dynamic component to avoid flash of unstyled content */
  • packages/next/src/shared/lib/lazy-dynamic/preload-chunks.tsx: // For stylesheets we actually need to render the CSS because nothing else is going to do it

Where we suspect this comes from — a pointer, not a diagnosis, as we don't know the Turbopack internals: PreloadChunks renders its link from opts.modules looked up in reactLoadableManifest, and the affected chunks are exactly the ones missing from that manifest. Looking for why, we noticed the DFS in crates/next-api/src/module_graph.rs (tag v16.3.4, around lines 204-210) records a dynamic entry and then returns GraphTraversalAction::Skip for it. If that means the traversal does not descend into a dynamically imported module, a dynamic() declared deeper would never be collected, which matches what we see. The skip itself looks like a sensible optimisation — we mention it only because the symptom lines up.

experimental.cssChunking is not an option here: Turbopack rejects it with "experimental.cssChunking is only supported with webpack". experimental.inlineCss: true does not change the behaviour.

Workaround that keeps the lazy loading: move the level-2 dynamic() declarations into a module reached by a plain import from a statically imported module. In our app this dropped late CSS modules from 29 to 10 (the remaining 10 are ssr: false components, where late CSS is expected) with byte-identical CSS output, the same chunk count, and code splitting intact — the variant still lands in its own 1.8 KB lazy chunk, and the route keeps 73 lazy chunks versus 58 when the same components are imported statically.

Related: #98287 (Turbopack omits __NEXT_DATA__.dynamicIds for next/dynamic in pages/_app; same class of missing registration), #98292 (PR filling another hole in the Turbopack loadable manifest), #83941 (Turbopack CSS prioritisation differs between dev and build).

Note: I first filed this as #98803 without a reproduction link and the bot closed it, as documented. Re-filing with the repro, per its instructions.