Skip to content

next/dynamic lazy chunk uses stale module graph after source edits (Turbopack) #93293

@Djangologie

Description

@Djangologie

Link to the code that reproduces this issue

https://github.com/Djangologie/turbopack-dynamic-stale-chunk-repro

To Reproduce

  1. npm install && npm run dev (Turbopack)
  2. Open http://localhost:3000, click "Seed 12 items" — cards render with [CATEGORY] prefix
  3. Edit src/lib/format-utils.ts: change [${cat.toUpperCase()}] to >> ${cat.toUpperCase()} <<
  4. Save the file

Expected: HMR updates the cards to show the new format.

Actual: One of:

  • Cards still show old format [REACT] (stale chunk — hash unchanged)
  • Crash: TypeError: formatCategory is not a function (module factory evicted but chunk not re-linked)
  • Crash: Error: Module factory is not available. It might have been deleted in an HMR update.

The stale chunk persists across hard refresh, rm -rf .next + server restart, and even full node_modules reinstall. Only next build (Webpack) resolves it.

Additional context

This compounds severely with persisted Zustand stores (see src/stores/item-store.ts):

  1. First visit: store empty → SSR and client match → no issue
  2. User seeds items → persisted to localStorage
  3. Page reload: server renders 0 items, client hydrates with 12 items from localStorage
  4. Structural hydration mismatch triggers React 19 error recovery
  5. Combined with stale dynamic chunk, recovery produces corrupted DOM: NaN of NaN pagination, missing CSS classes, broken refs

Workaround: Replace next/dynamic with a static import — confirms the issue is specific to the lazy chunk path in Turbopack.

- const ItemCard = dynamic(() => import("./ItemCard"), { ssr: false })
+ import ItemCard from "./ItemCard"

next build + next start works correctly in all cases.

Current vs. Expected behavior

Current: next/dynamic chunks in Turbopack retain stale module references after source edits. The chunk hash does not update, so browsers serve cached stale code indefinitely.

Expected: Editing a module imported by a dynamically-loaded component should invalidate the lazy chunk and generate a new hash, just like Webpack does in production builds.

Provide environment information

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 23.6.0
  Available memory (MB): 65536
  Available CPU cores: 12
Binaries:
  Node: 22.14.0
  npm: 10.9.2
  Yarn: N/A
  pnpm: 10.5.2
Relevant Packages:
  next: 16.1.6
  eslint-config-next: N/A
  react: 19.2.4
  react-dom: 19.2.4
  typescript: 5.8.3
Next.js Config:
  output: N/A

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

Turbopack

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

next dev (local)

Additional context

This was originally filed as #93292 and closed by the bot for missing a reproduction link. This is the re-filed version with a complete minimal reproduction.

Metadata

Metadata

Assignees

No one assigned

    Labels

    TurbopackRelated to Turbopack with Next.js.

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions