Store the Turbopack module cache in a Map - #98947
lukesandberg wants to merge 1 commit into
Conversation
Failing test suitesCommit: 3922b78 | About building and testing Next.js
Expand output● app-dir action body finalize with nodejs middleware and output-standalone › should handle large payload through server action after nodejs middleware with delayed body finalize
Expand output● turbopack additional roots › resolves a linked package, sibling dependency, and next/dist ● turbopack additional roots › reports initialization warnings when startup succeeds ● turbopack additional roots › emits additional-root files and cross-root symlinks in the NFT ● turbopack additional roots › runs after relocating standalone output away from the source root
Expand output● output: standalone with twoslash › should annotate twoslash types default ● output: standalone with twoslash › should annotate twoslash types esnext
Expand output● turbopack additional roots › resolves a linked package, sibling dependency, and next/dist ● turbopack additional roots › reports initialization warnings when startup succeeds ● turbopack additional roots › emits additional-root files and cross-root symlinks in the NFT ● turbopack additional roots › runs after relocating standalone output away from the source root
Expand output● Graceful Shutdown › production (standalone mode) › should wait for requests to complete before exiting ● Graceful Shutdown › production (standalone mode) › should not accept new requests during shutdown cleanup › should finish pending requests but refuse new ones ● Graceful Shutdown › production (standalone mode) › should not accept new requests during shutdown cleanup › should stop accepting new requests when shutting down
Expand output● next-server-nft › with output:standalone › should not trace too many files in next-server.js.nft.json ● next-server-nft › with adapters and output:standalone › should emit both whole-app server NFTs and complete the build
Expand output● pnpm support › build with dependencies installed via pnpm › should build with dependencies installed via pnpm ● pnpm support › standalone mode with client-side JS › should execute client-side JS on each page in output: "standalone"
Expand output● Required Server Files › production mode › should output required-server-files manifest correctly ● Required Server Files › production mode › should render SSR page correctly ● Required Server Files › production mode › should render dynamic SSR page correctly ● Required Server Files › production mode › should render fallback page correctly ● Required Server Files › production mode › should render SSR page correctly with x-matched-path ● Required Server Files › production mode › should render dynamic SSR page correctly with x-matched-path ● Required Server Files › production mode › should render fallback page correctly with x-matched-path and routes-matches ● Required Server Files › production mode › should return data correctly with x-matched-path ● Required Server Files › production mode › should render fallback optional catch-all route correctly with x-matched-path and routes-matches ● Required Server Files › production mode › partial optional catch-all route › should render /partial-catch-all/hello.com ● Required Server Files › production mode › partial optional catch-all route › should render /partial-catch-all/hello.com/hello ● Required Server Files › production mode › partial optional catch-all route › should render /partial-catch-all/hello.com/hello/world ● Required Server Files › production mode › should return data correctly with x-matched-path for optional catch-all route ● Required Server Files › production mode › should not apply trailingSlash redirect ● Required Server Files › production mode › should normalize catch-all rewrite query values correctly ● Required Server Files › production mode › should bubble error correctly for gip page ● Required Server Files › production mode › should bubble error correctly for gssp page ● Required Server Files › production mode › should bubble error correctly for gsp page ● Required Server Files › production mode › should normalize optional values correctly for SSP page ● Required Server Files › production mode › should normalize optional values correctly for SSG page ● Required Server Files › production mode › should normalize optional values correctly for API page ● Required Server Files › production mode › should match the index page correctly ● Required Server Files › production mode › should match the root dynamic page correctly ● Required Server Files › production mode › should preserve dynamic route identity from x-matched-path ● Required Server Files › production mode › should rematch the concrete path after using x-matched-path ● Required Server Files › production mode › should handle 404s properly
Expand output● required server files app router › should send the right cache headers for an app route ● required server files app router › should handle optional catchall ● required server files app router › should send the right cache headers for an app page ● required server files app router › should not fail caching ● required server files app router › should properly handle prerender for bot request ● required server files app router › should properly handle fallback for bot request ● required server files app router › should send cache tags in minimal mode for ISR ● required server files app router › should not send cache tags in minimal mode for SSR ● required server files app router › should not send invalid soft tags to cache handler ● required server files app router › should not override params with query params ● required server files app router › should de-dupe HTML/RSC requests for ISR pages ● required server files app router › should isolate cache between different ISR request groups
Expand output● standalone mode - no app routes › should handle pages rendering correctly
Expand output● standalone mode: server action externals › should execute a server action that uses external packages
Expand output● cache-components OTEL spans › should allow creating Spans during prerendering during the build - inside a Cache Components ● cache-components OTEL spans › should allow creating Spans during prerendering at runtime - inside a Cache Components ● cache-components OTEL spans › should allow creating Spans during resuming a fallback - inside a Cache Component ● cache-components OTEL spans › should allow creating Spans from a tracer acquired before provider registration
Expand output● og-api › should respond from index ● og-api › should work in pages/api ● og-api › should work in app route ● og-api › should work in app route in node runtime after image optimization ● og-api › should work in middleware ● og-api › should copy files correctly ... truncated to fit in one GitHub comment ... |
The runtime kept instantiated modules in a plain object keyed by module id. Module ids are arbitrary strings, so that object lands in dictionary mode and every lookup is a megamorphic property access; `delete` (used throughout HMR) is worse still. Switch the cache to a `Map`. The blocker was `require.cache`, which is specified as a plain object and so can't be a `Map` directly. Rather than keep the object for everyone's benefit, `require.cache` accesses are now rewritten into an import of a new `@turbopack/module-cache` helper that wraps the `Map` in a `Proxy` with object semantics. Going through a real module reference means the shim is only pulled into bundles that actually touch `require.cache`, which is rare. `ModuleId` is `string | number` and production builds use numeric ids, while property keys always reach a `Proxy` trap as strings. The id kind is fixed for a build, and the shim is itself a module, so it reads its own id to decide the conversion once and converts keys accordingly, stringifying them again on the way out of `ownKeys`. Deciding up front rather than probing for both keeps each trap to a single `Map` lookup, and gives writes a definite answer for a key that isn't cached yet — probing would have to guess, and guessing wrong strands an entry that `Object.keys` reports but `require()` can never find. The execution suite covers both id shapes: the new `numeric-ids` variant builds with the numeric id strategy, enabled by a `numericModuleIds` test option. With that, `__turbopack_cache__` no longer needs to be reachable from user code, so it's dropped from the free-var list along with the now-unused `CjsRequireCacheAccess` codegen. Measured with bench/module-cost (9331 modules per flavor, 40 samples per arm, both arms built the same way). Module execution time, the phase that actually hits the cache: client commonjs -4.4% 95% CI [-4.7%, -2.2%] pages api commonjs -4.4% 95% CI [-6.3%, -3.4%] pages api esm -2.1% 95% CI [-3.2%, -1.4%] client esm +0.2% (noise) Load times are flat, as expected. CommonJS shows the larger relative gain, but not because it does more cache lookups: both flavors route through `getOrInstantiateModuleFromParent` once per import. ESM just does more work per module on top of that lookup (namespace setup, `esmExport`/`interopEsm`), so the lookup is a smaller fraction of its total. Note this benchmark is a deep require chain and deliberately slams the codepath, so treat it as an upper bound rather than a typical app. The benchmark runner now keeps raw per-sample timings alongside the summary statistics; without them an A/B can't tell a real difference from run-to-run noise. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
92dd009 to
3922b78
Compare
Maps are faster than Objects for map-like usecases such as the module cache
The one complexity is
require.cachewhich exposes the module cache as an object, this however is an extremely rare usecase, so we support it via a runtime module that re-exposes the module cache as an Object using aProxy.Performance
Measured with
bench/module-costModule execution time — the phase that actually hits the cache:
Caveat: this benchmark is a deep require chain that deliberately slams this codepath, so this really just confirms the claim that Maps are faster and this is relevant to the bundled codepaths.