Turbopack browser runtime: first-wins module registration installs the wrong factory when a module id is emitted with different bodies in different chunks
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 142k
- Forks
- 32.4k
- Avg merge
- 2d 14h
- Merged PRs (30d)
- 351
Description
Link to the code that reproduces this issue
https://github.com/Stanzilla/turbopack-first-wins-repro
To Reproduce
npm install
npm run verify
verify builds the app and runs machine-checked static, natural-runtime, and synthetic-runtime assertions. No browser or dev server is required. The scripts find generated chunk names and module IDs from .next, so they do not depend on fixed hashes.
Current vs. Expected behavior
In this Pages Router app, /dyn-a imports lib/heavy.js dynamically. /dyn-b imports the same module both statically and dynamically. Turbopack emits the same async-loader module ID in both page chunks, but with different factories:
/dyn-a: load the target chunk, then require the target module/dyn-b: usePromise.resolve, then require the target module because its factory is assumed to be present
The production browser runtime uses the first factory registered for a module ID. npm run repro evaluates the build's real, unmodified runtime and page chunks in both orders. One order installs the chunk-loading factory. The other installs the Promise.resolve factory. The installed behavior therefore depends on script execution order.
If the Promise.resolve factory wins before the target factory is registered, the dynamic import can fail with:
Module <X> was instantiated because it was required from module <Y>,
but the module factory is not available.
The repro also includes a synthetic overlap case. It sends two compressed module groups through the real runtime. The groups share one ID but have different factories. The runtime reuses the first group's factory for every new ID in the second group and deterministically produces the error above. A control case passes without the overlap.
Expected: a module ID has one installed behavior regardless of chunk execution order. Turbopack should emit equivalent factories, use context-specific IDs, or reject a conflicting registration instead of silently selecting the first factory.
We see the same error intermittently on cold loads in a large Pages Router app on iOS WKWebView. One production build has about 3,300 chunks and 59 module IDs with divergent loader factories. On 9 pages, the Promise.resolve variant registers first in document order while the target factory is in another page chunk.
Provide environment information
Operating System:
Platform: darwin
Arch: arm64
Version: Darwin Kernel Version 25.6.0
Binaries:
Node: 24.19.0
npm: 11.17.0
Relevant Packages:
next: 16.3.3
react: 19.2.8
react-dom: 19.2.8
Also reproduced on Next.js 16.4.0-canary.12.
Which area(s) are affected? (Select all that apply)
Turbopack, Pages Router, Runtime
Which stage(s) are affected? (Select all that apply)
next build (local), Other (Deployed)
Additional context
The current group-factory reuse behavior was added in https://github.com/vercel/next.js/pull/89976.
Probable code path:
AsyncLoaderModule::chunk_item_contentchanges the generated factory based on chunk availability.AsyncLoaderModule::asset_ident_foridentifies the loader from its target module, and global module ID generation collects that identity.installCompressedModuleFactoriespreserves the first registered factory.
chunk_item_content_ident already adds availability information, but the build still gives the two factory bodies the same numeric module ID. A likely repair is to make the loader body availability-independent or make the emitted identity availability-specific. Simply preferring the last factory would conflict with the compressed-group consistency that https://github.com/vercel/next.js/pull/89976 preserves.
experimental.turbopackScopeHoisting: false is our current workaround. It removes merged groups from the affected production build with a 0.4% gzip size increase. It does not remove the divergent async-loader factories.
Scope boundary: the natural app proves divergent factories and order-dependent installation. The overlap crash uses synthetic registration tuples with the real runtime. We did not find non-identical overlapping groups in one clean build, and we have not proved which path causes the production failures.
Probably related: https://github.com/vercel/next.js/issues/86132
The reproduction and measurements were prepared with AI assistance (Claude Code, claude-fable-5) and reviewed by the reporting team.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Run npm install, npm run verify, and npm run repro in the linked reproduction first. Then read AsyncLoaderModule::chunk_item_content and asset_ident_for in turbopack/crates/turbopack-ecmascript/src/async_chunk/module.rs, global ID generation in turbopack/crates/turbopack/src/global_module_ids.rs, and installCompressedModuleFactories in turbopack-ecmascript-runtime/js/src/shared/runtime/runtime-utils.ts. Done means the real runtime no longer selects order-dependent behavior for conflicting module registrations, with regression coverage for the reported overlap.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, nextjs, react, rust, typescript
- Domain
- build-system, tooling, web-dev
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100