Repository navigation
Conversation
Contributor
Tests PassedCommit: 62d66a2 |
This was referenced Sep 22, 2026
sokra
added this pull request to stack #98824
September 22, 2026 17:25
sokra
force-pushed
the
codex/turbopack-keyed-chunk-group-index
branch
from
September 22, 2026 17:29
ef1ab48 to
af5b337
Compare
sokra
commented
Sep 23, 2026
sokra
commented
Sep 23, 2026
sokra
force-pushed
the
codex/turbopack-keyed-chunk-group-index
branch
from
September 23, 2026 10:47
af5b337 to
17e8e12
Compare
sokra
removed this pull request from stack #98824
September 23, 2026 13:08
sokra
added this pull request to stack #99101
September 23, 2026 13:08
sokra
force-pushed
the
codex/turbopack-keyed-chunk-group-index
branch
from
September 23, 2026 14:00
17e8e12 to
fd0bbf2
Compare
sokra
force-pushed
the
codex/turbopack-keyed-chunk-group-index
branch
from
September 24, 2026 08:22
fd0bbf2 to
3f74d26
Compare
sokra
added a commit
that referenced
this pull request
Sep 24, 2026
### What? Stops Turbopack from re-chunking every App Router layout segment once per page in production builds. The manually constructed server-utilities chunk group is now only created in development. ### Why? A production build should chunk each shared layout segment exactly once. Instead, a layout segment was chunked again for every endpoint below it. On a small app with four endpoints, the root layout was chunked 4 times and a nested `dashboard` layout 2 times. The emitted output was still shared, so this was wasted build work rather than bigger bundles, which is why output-based tests never caught it. Production already builds a single module graph for the whole app, so the module graph is not what differs between endpoints. The difference comes from the server-utilities chunk group that seeds the layout-segment chain: - It is a `SharedMerged` group whose parent is the current endpoint's entry chunk group. - A merged group's parent is part of its identity, so every endpoint gets a different group (5 chunkings on the same app). - Its result seeds the availability info passed to each layout segment after it. Availability info is part of the chunking task key, so each shared segment ended up with a different key per endpoint and was chunked again. The code comment already said this manual shared chunk exists mainly to help development. That makes sense there: each endpoint has its own module graph in development, and without the group layout segments would share very little across pages. This is PR 0 of the chunk-group stack. The PRs above it are: 1. #98733 — canonical chunk-group identity 2. #98734 — graph-derived output groups 3. #98823 — Pages Router dev client graph chaining 4. #98685 — chunk-group availability bitmaps 5. #99050 — keyed chunk-group index lookups ### How? The manual server-utilities group is now created only in per-page-module-graph mode, which is development. In production the layout-segment chain starts from an endpoint-independent state, so sibling endpoints produce the same chunking task key for a shared segment. The server utilities are then chunked with the entry through the whole-app graph. Development behaves exactly as before. On the same four-endpoint app, a production build now chunks the root layout and the nested layout **once each**, and the manual server-utilities group is gone. Development still creates the group and chunks segments per page, which is expected for on-demand compilation. Adds `test/e2e/app-dir/layout-segment-chunk-sharing`, a root layout and a nested layout that share one module across two sibling pages. It asserts two things: - **Output sharing (Turbopack dev and production):** the shared module is emitted into exactly one server chunk. Webpack builds a self-contained server bundle per route, so this assertion is gated on the bundler, not the mode. - **Work sharing (Turbopack production only):** output sharing alone does not prove the chunking *task* was reused, since a segment could be re-chunked per page with an identical result. The test builds with tracing enabled and uses `next internal query-trace` to count executions of the nested layout's chunking task, which must be exactly one. A cache hit produces no execution span, so the count equals the number of real executions. Development compiles pages on demand with a module graph per page, so repeated chunking is expected there and the assertion is skipped. With the fix reverted, the work-sharing assertion fails with 2 executions, while the output-sharing assertion still passes. ### Performance Measured on a synthetic App Router app: a root layout and a nested `section/layout.tsx`, each importing M modules (~20% `'use client'`), with N trivial sibling pages under `section/`. Cold `next build` with release native builds of both commits (`192fcaf7` vs this PR, one shared JS build); paired runs in ABBA order on an 8 vCPU / 16 GB Vercel Sandbox VM; 95% Student-t intervals on the paired differences. At **800 pages × 1,000 modules per layout**, a dedicated 12-pair run: | metric | Δ | 95% CI | pairs improved | |---|---|---|---| | Turbopack compile time | **−6.3%** (−767 ms) | [−968, −565] ms | 12/12 | | total `next build` time | **−2.2%** (−764 ms) | [−1241, −287] ms | 9/12 | | Turbopack process peak RSS | **−4.2%** (−104 MB) | [−141, −68] MB | 12/12 | In traces of that build, the nested layout goes from 800 chunking executions to 1, the root layout from 802 to 1, and the server-utilities group from 803 to 0 (Node.js chunking spans overall: 7,223 → 1,617). Smaller configurations (50–800 pages × 200 modules, 200 pages × 1,000 modules; 8 pairs each) showed no detected difference in time or Turbopack memory, so the gain matters mainly for large apps. **Trade-off:** without the manual shared group, a few server utilities (the `*-async-storage.external` and `runtime-reacts.external` wrappers, the vendored `react-rsc` getter and a small `bytes` helper) are now inlined into each page's SSR entry chunk instead of being emitted once. That adds about **2.3 KB of server output per App Router endpoint** (+0.9% to +4.4% of server JS in these fixtures). Client JS is byte-identical. Keeping these modules shared in production without per-endpoint identity would be a possible follow-up. ### Verification - Trace measurement on the fixture (production): root layout 4 → 1, nested layout 2 → 1, server-utilities group 5 → 0; dev unchanged - Production build renders and serves every route with the shared module; the shared module is still emitted into exactly one server chunk - `layout-segment-chunk-sharing` — Turbopack production (3 passed), Turbopack dev (output assertion passed, trace assertion skipped), webpack production (Turbopack-only assertions skipped) - `cargo clippy --workspace --all-targets -- -D warnings -A deprecated`, `cargo fmt --all -- --check` - `turbopack-core` + `next-core` unit tests (252 passed); `turbopack-tests` snapshots (126 passed, no changes) <!-- NEXT_JS_LLM --> <!-- fleet 4f67c6f7-bbe2-454b-9a17-822a60ef8f7e --> Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com> Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
sokra
force-pushed
the
codex/turbopack-keyed-chunk-group-index
branch
from
September 24, 2026 16:14
3f74d26 to
64a5ed3
Compare
sokra
force-pushed
the
codex/turbopack-keyed-chunk-group-index
branch
from
September 24, 2026 16:45
64a5ed3 to
84105dd
Compare
sokra
force-pushed
the
codex/turbopack-keyed-chunk-group-index
branch
from
September 30, 2026 18:02
84105dd to
f91631e
Compare
sokra
force-pushed
the
codex/turbopack-keyed-chunk-group-index
branch
from
October 2, 2026 08:59
f91631e to
9599468
Compare
sokra
force-pushed
the
codex/turbopack-keyed-chunk-group-index
branch
from
October 5, 2026 14:37
9599468 to
3fc5dd3
Compare
sokra
removed this pull request from stack #99101
October 5, 2026 14:38
sokra
added a commit
that referenced
this pull request
Oct 9, 2026
### What? Makes `ChunkGroupInfo` resolve chunk groups through their canonical `ChunkGroupKey` identity. ### Why? Merged chunk groups carry an entry list whose order is intentionally unspecified. Looking up the full `ChunkGroup` value therefore makes identity depend on incidental entry ordering, even though the module graph already indexes groups by parent and merge tag. This is PR 1 of an eight-PR stack (on top of #99100, already merged): 1. **This PR** — canonical chunk-group identity 2. #99686 — chunk several chunk groups in one `chunk_group` call 3. #99749 — register the chunk groups that are chunked 4. #98734 — use graph-derived merged groups 5. #99750 — chunking takes a `ChunkGroupKey` 6. #98823 — Pages Router dev client graph chaining 7. #98685 — chunk-group availability bitmaps 8. #99050 — keyed chunk-group index lookups It provides stable identity for the graph-derived chunk-group work above it. ### How? Adds a canonical key conversion for every chunk-group variant and uses the existing `chunk_group_keys` index for lookup. Merged keys intentionally contain only their parent group and merge tag. ### Verification - `cargo test -p turbopack-core` — 160 passed - `cargo test -p turbopack-tests --test snapshot` — 125 passed - Affected Cargo checks and Rust formatting <!-- NEXT_JS_LLM --> <!-- fleet 4f67c6f7-bbe2-454b-9a17-822a60ef8f7e --> Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com> Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
sokra
added a commit
that referenced
this pull request
Oct 9, 2026
…ll (#99686) ### What? `ChunkingContext::chunk_group` takes a list of chunk groups and chunks them together, as one chunk group, in a single chunking task. It produces one `ChunkGroupResult` (one `make_chunks` run) for the whole list. `evaluated_chunk_group`, `entry_chunk_group` and the `ChunkingContextExt` helpers still take a single group and wrap it in a one-element list. All existing callers pass a one-element list, so **this PR changes no output** (zero snapshot changes). ### Why? Review feedback on #98734 ([1](#98734 (comment)), [2](#98734 (comment))): a layout segment's client references can derive several isolated merged chunk groups, and #98734 chunked each one with a separate `chunk_group` call. That costs an extra chunking task per group and threads availability by hand. This PR adds the list form; #98734 then calls `chunk_group` once per segment and side with all of the segment's derived groups. ### How? `make_chunk_group` / `chunk_group_content` / `chunk_group_content_operation` take the list as a `SmallVec<[ChunkGroup; 1]>`: almost every call passes a single group, so the common case doesn't heap-allocate. The list crosses turbo-task boundaries, so this PR also adds `impl TaskInput for SmallVec<A>` in turbo-tasks, mirroring the `Vec` impl. Semantics: - **Traversal:** the entries of each group, in list order, with one shared traversal state. A module already reached from an earlier group isn't collected again, which matches what the previous one-call-per-group sequence produced (CSS order unchanged). - **Availability:** every listed group is added to the resulting availability info. - **Entry groups:** if several `Entry` groups are listed, the active page entries and the resulting entry group are the union of their modules, in list order. - **Edge cases:** an empty list is an error; groups listed more than once (same key) are chunked once. Unit tests cover the edge cases, the `Entry` union, and `SmallVec` as a task input (inline and spilled). This is PR 2 of an eight-PR stack (on top of #99100, already merged): 1. #98733 — canonical chunk-group identity 2. **This PR** — chunk several chunk groups in one `chunk_group` call 3. #99749 — register the chunk groups that are chunked 4. #98734 — use graph-derived merged groups 5. #99750 — chunking takes a `ChunkGroupKey` 6. #98823 — Pages Router dev client graph chaining 7. #98685 — chunk-group availability bitmaps 8. #99050 — keyed chunk-group index lookups <!-- NEXT_JS_LLM --> <!-- fleet 4f67c6f7-bbe2-454b-9a17-822a60ef8f7e --> Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com> Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
sokra
added a commit
that referenced
this pull request
Oct 9, 2026
### What? Registers chunk groups in the module graph exactly as output code chunks them, in two places: - **Pages layout segments**: `_document` and `_app` are registered as their own `Entry` groups (`layout_chunk_group_entry`), in both the per-page (dev) graph chain and the whole-app `entries()`. `internal_ssr_chunk` chunks them as `ChunkGroup::Entry(vec![layout])` instead of `ChunkGroup::Shared(layout)`. - **turbopack-cli dev**: `create_web_entry_source` registers one `Entry` group per entry, with the same entries `DevHtmlAsset` chunks: the runtime entries followed by the entry for evaluatable modules, or the module alone otherwise. It passes `runtime_entries` without `with_entry(entry)`, because `DevHtmlAsset` appends the entry itself. ### Why? A chunk group is identified by its key, and for an `Entry` group that is its ordered entries. The next PR in the stack looks every chunked group up in `ChunkGroupInfo` by key, so a chunked group the graph didn't register fails. - Pages layouts were declared `Shared(layout)`. The whole-app graph also registers the standalone `/_document` and `/_app` endpoints' `Entry([module])` group for the same module and keeps only one root group per module, so `Shared(layout)` wasn't registered there. - turbopack-cli registered a single `Entry` group with all entries and runtime entries, while `DevHtmlAsset` chunks one group per entry (with the entry appended twice). No output change is intended: the same modules are chunked, but the registered groups now match them. This is PR 3 of an eight-PR stack (on top of #99100, already merged): 1. #98733 — canonical chunk-group identity 2. #99686 — chunk several chunk groups in one `chunk_group` call 3. **This PR** — register the chunk groups that are chunked 4. #98734 — use graph-derived merged groups 5. #99750 — chunking takes a `ChunkGroupKey` 6. #98823 — Pages Router dev client graph chaining 7. #98685 — chunk-group availability bitmaps 8. #99050 — keyed chunk-group index lookups ### Verification - Workspace `cargo clippy --all-targets -D warnings`, `cargo check --workspace --all-targets`, `cargo fmt --check` - Turbopack snapshots: 132 passed, no snapshot changes - `turbo-tasks` + `turbopack-core` + `next-core` unit tests: 362 passed - Turbopack production and dev `404-page-router` (custom `_document` / `_app`): 25/25, 12/12 <!-- NEXT_JS_LLM --> <!-- fleet 4f67c6f7-bbe2-454b-9a17-822a60ef8f7e --> Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com> Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
sokra
force-pushed
the
codex/turbopack-keyed-chunk-group-index
branch
from
October 9, 2026 12:20
c171d17 to
9662000
Compare
sokra
removed this pull request from stack #99752
October 9, 2026 14:22
sokra
changed the base branch from
codex/turbopack-chunk-group-availability
to
canary
October 9, 2026 14:23
sokra
force-pushed
the
codex/turbopack-keyed-chunk-group-index
branch
from
October 9, 2026 14:25
9662000 to
f21e085
Compare
sokra
changed the base branch from
canary
to
codex/turbopack-chunk-group-availability
October 9, 2026 14:26
sokra
added this pull request to stack #99941
October 9, 2026 14:26
sokra
added a commit
that referenced
this pull request
Oct 9, 2026
…8823) ### What? Chains the Pages Router **development** client module graph so that `/_app` and the page being chunked live in the same `ModuleGraph`. This is PR 1 of a six-PR stack, based on `canary` (#98733, #99686 and #99749 are already merged): 1. **This PR** — Pages Router dev client graph chaining 2. #99910 — regression test for Pages Router `_app` availability (#99789) 3. #98734 — use graph-derived merged groups 4. #99750 — chunking takes a `ChunkGroupKey` 5. #98685 — chunk-group availability bitmaps 6. #99050 — keyed chunk-group index lookups ### Why? In development `Project::per_page_module_graph()` is true, so every page endpoint builds its own client `ModuleGraph`, each with its own `ChunkGroupInfo` and its own index space starting at `0`. `/_app` is always loaded before the page in the browser, so the page reuses `/_app`'s availability when chunking. Today that availability is a set of concrete modules, which is graph-independent, so the split graphs go unnoticed. That stops being true as soon as availability is expressed as chunk-group indices (#98685): `/_app` would record index `0` in *its* graph, and the page would resolve that same `0` against *its own* graph — where it means the page's own entry group. Every module of the group being chunked then looks already-available and is dropped, producing runtime errors like: ``` Module [hmr-entry]/hmr-entry.js { ENTRY => "[project]/pages/_error" } was instantiated as a runtime entry of chunk …, but the module factory is not available. ``` ### How? - `PageEndpoint::client_module_graph` builds a graph *chain* in per-page mode: `/_app`'s entry group first, then the page's, with `VisitedModules` threaded between them — the same layout-segment approach `ssr_module_graph` already uses for document → app → page. - Whole-app mode (production) is unchanged and still goes through `module_graph_for_modules`. - `/_app` itself is not chained onto anything. - Extracts a small `client_entry_modules` helper, since the entry module list is now needed in two places. Availability is still carried as the set of modules `/_app` emitted. Making the two endpoints share one graph also fixes #99789: with separate graphs, batching in the page graph could produce a node for a module shared with `/_app` whose edges point at page-only modules, and those modules were skipped as already available ("module factory is not available" on hydration). #99910 adds the regression test. Lining up the graph identities is also what #98685 builds on. ### Verification - `turbopack-core` — 160 passed - `next-core` — 84 passed - Turbopack snapshots — 125 passed - `404-page-router` and `server-navigation-error` development suites - Affected Cargo checks and Rust formatting <!-- NEXT_JS_LLM --> <!-- fleet 4f67c6f7-bbe2-454b-9a17-822a60ef8f7e --> Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com> Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
sokra
added a commit
that referenced
this pull request
Oct 9, 2026
…99910) ### What? Regression coverage for #99789, stacked on #98823. This is PR 2 of a six-PR stack, based on `canary`: 1. #98823 — Pages Router dev client graph chaining 2. **This PR** — regression test for Pages Router `_app` availability (#99789) 3. #98734 — use graph-derived merged groups 4. #99750 — chunking takes a `ChunkGroupKey` 5. #98685 — chunk-group availability bitmaps 6. #99050 — keyed chunk-group index lookups - **New e2e suite** `test/e2e/pages-app-availability-boundary`: a Pages Router app with the import shape from #99789: - `_app` imports `c-shared` and dynamically imports a module that also imports it. - `pages/index` imports `a-page-only`, `b-boundary`, `c-shared`, `d-boundary`, `e-page`, and dynamically imports a module that imports `b`/`d`, which makes `b` and `d` batching boundaries. The test asserts that the page hydrates (a marker set in `useEffect`), that every imported value renders, and that no "module factory is not available" error is logged. - **`app-document-import-order`**: the "loads modules shared by _app and the page only once" assertion added in #97664 was skipped in all of dev. It now runs everywhere and is gated `// @gate !(turbopack && dev)` (expected failure). ### Why? In Turbopack dev, #97664 seeds each page's client chunk group with `/_app`'s availability. But each page had its own module graph, so availability items (`ModuleOrBatch` identities) were compared across graphs. In the page graph, batching can produce a `ModuleOrBatch::Module(c-shared)` node whose edges point at earlier siblings (`a`, `b`). Because `/_app` already has `Module(c-shared)`, the traversal excluded that subtree, and `a`/`b` were never chunked. Hydration then fails with `Module [project]/lib/a-page-only.js … was instantiated … but the module factory is not available`. No test covered this, because the #97664 test was skipped in dev. #98823 chains `/_app` and the page into one dev client module graph, which fixes the missing modules. It does **not** yet dedupe the `app-document-import-order` fixture in Turbopack dev. The shared module is still emitted in both the `_app` and the page chunk, because the page's batches in the combined graph carry different chunk-group bitmaps than `_app`'s own graph, so they don't match `_app`'s availability items. Hence the gate. It stays valid through #98734 and #99750; #98685 (higher in this stack) dedupes the fixture in Turbopack dev, turning it into a "stale gate", and removes it. ### How was it verified? Local JS build of this branch plus the preview native binaries of #98823's head (`8dd759a`) and its base (`26fd598`) in `packages/next-swc/native`. The two commits differ only in Rust. | Suite | Mode | #98823 base | #98823 head | |---|---|---|---| | `pages-app-availability-boundary` | Turbopack dev | ❌ module factory not available | ✅ | | `pages-app-availability-boundary` | Turbopack start, webpack dev/start | | ✅ | | `app-document-import-order` | Turbopack dev | | ✅ (gated: failed as expected) | | `app-document-import-order` | webpack dev, Turbopack/webpack start | | ✅ | After restacking onto canary, with native builds of each layer, in Turbopack dev: - This PR's tree: `pages-app-availability-boundary` ✅, `app-document-import-order` ✅ (gated: failed as expected). - #98734 and #99750: gated assertion still fails as expected. - #98685 and #99050: gate stale, so #98685 removes it; ungated, both suites ✅. Fixes #99789 (together with #98823) <!-- NEXT_JS_LLM --> <!-- fleet a7614af3-e186-4297-beaf-7e55eaddc0da --> <!-- fleet 4f67c6f7-bbe2-454b-9a17-822a60ef8f7e --> Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com> Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com> Co-authored-by: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com>
sokra
force-pushed
the
codex/turbopack-keyed-chunk-group-index
branch
from
October 9, 2026 22:15
f21e085 to
9785538
Compare
lukesandberg
marked this pull request as ready for review
October 14, 2026 21:18
lukesandberg
approved these changes
Oct 14, 2026
sokra
force-pushed
the
codex/turbopack-keyed-chunk-group-index
branch
from
October 15, 2026 07:53
9785538 to
575d570
Compare
sokra
force-pushed
the
codex/turbopack-keyed-chunk-group-index
branch
from
October 15, 2026 09:02
575d570 to
82ab6bf
Compare
`ChunkGroupInfo::get_index_of` read `chunk_group_keys` off the whole `ChunkGroupInfo`, so every index lookup in the graph depended on all of it and re-executed whenever any unrelated chunk group changed. Add `ChunkGroupKeyToIndex`, a `cell = "keyed"` map from `ChunkGroupKey` to its index, and resolve lookups through `Vc::get` so a lookup depends only on the key it asks for. The existing `FxIndexSet<ChunkGroupKey>` cannot serve this: `KeyedAccess for IndexSet` yields `()` rather than the index, and `KeyedEq for IndexSet` compares membership while ignoring position, so a key whose index merely shifted would not be reported as changed and lookups would return a stale index. Mapping to the index as the value makes `KeyedEq for HashMap` compare indices, which detects exactly that. The set stays for the reverse (index -> key) direction. The debug-only "couldn't find chunk group" path still reads the whole struct; it is only taken on failure, so the happy path keeps its narrow dependency. Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
sokra
force-pushed
the
codex/turbopack-keyed-chunk-group-index
branch
from
October 15, 2026 10:10
82ab6bf to
62d66a2
Compare
sokra
added a commit
that referenced
this pull request
Oct 24, 2026
### What? Next.js output chunking uses the merged chunk groups derived by `ChunkGroupInfo`, rather than reconstructing them in output code: - **Client references** (`app_client_references_chunks.rs`): each layout segment chunks the isolated merged client reference groups the graph derived for its references, with one `chunk_group` call per segment and side (`ssr modules`, `client modules`, using the list form from #99686). There is no manual availability threading or result concatenation. - **Server utilities** (`app.rs`, per-page module graph): the entry's server utilities are chunked as the graph's shared merged group for the entry and the server utility merge tag, instead of a hand-built `SharedMerged` group. - **Ordered traversal**: chunk traversal starts from `ModuleBatchesGraph::get_ordered_entries` for each group, in list order. The chunking API still takes `ChunkGroup` values; this PR passes the groups exactly as the graph registered them. #99750 then switches the API to keys. This is PR 3 of a six-PR stack, based on `canary` (#98733, #99686 and #99749 are already merged): 1. #98823 — Pages Router dev client graph chaining 2. #99910 — regression test for Pages Router `_app` availability (#99789) 3. **This PR** — use graph-derived merged groups 4. #99750 — chunking takes a `ChunkGroupKey` 5. #98685 — chunk-group availability bitmaps 6. #99050 — keyed chunk-group index lookups ### Why? The module graph derives merged parents from actual membership bitmaps. Output code previously guessed a parent and assembled an entry list by hand, which could disagree with the graph for shared client references and framework routes. A segment's client references can derive several merged groups (e.g. one per page rendering the root layout). Chunking each one separately ran one chunking task per group; chunking them together runs one per segment and side. Graph-derived merged entries have unspecified raw order, so chunk traversal must use the postorder `ModuleBatchesGraph` already computes, to preserve CSS evaluation order. ### How? - `ChunkGroupInfo` exposes exact memberships (`get_chunk_groups_for_module`) and the shared merged child of a parent and merge tag (`get_shared_merged_chunk_group`). - `ClientReference` retains its source module (`parent_module`), so merge-tagged groups are scoped to their graph parent. Component-owned references are intersected with their server component's memberships, which prevents collecting across segments, falling back to the source module's groups when the intersection is empty. - The client reference merge tags are shared `pub const`s in `next_client_reference` (`CLIENT_MERGE_TAG`, used by CSS and ecmascript client references, and `SSR_MERGE_TAG`), so producers and the chunking code match on the same values. - The `server side rendering` / `client side rendering` tracing spans are kept around the per-segment chunking. ### Verification - Workspace `cargo clippy --all-targets -D warnings`, `cargo check --workspace --all-targets`, `cargo fmt --check` - Turbopack snapshots: 132 passed, no snapshot changes - `turbo-tasks` + `turbopack-core` + `next-core` unit tests: 364 passed (incl. the new parent-index and merge-tag tests) - Production `layout-segment-chunk-sharing`, with a new assertion that the root layout's client and SSR modules are each chunked by one task. The suite now waits until the trace server has fully read the trace before querying spans. - Turbopack production and dev: `404-page-router`, `app-dir/actions/app-action`, `app-dir/initial-css-order`; production `edge-pages-support` <!-- NEXT_JS_LLM --> <!-- fleet 4f67c6f7-bbe2-454b-9a17-822a60ef8f7e --> Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com> Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
sokra
added a commit
that referenced
this pull request
Oct 24, 2026
### What?
Chunking APIs take chunk group keys instead of chunk groups:
- `ChunkingContext::chunk_group` takes `chunk_groups:
SmallVec<[ChunkGroupKey; 1]>`.
- `evaluated_chunk_group`, `entry_chunk_group` and all
`ChunkingContextExt` helpers take `chunk_group: ChunkGroupKey`.
- The browser and Node.js chunking contexts resolve each key with the
new `ChunkGroupInfo::get_chunk_group(key)` and chunk the registered
group, using its entries for evaluation and bootstrapping.
- Callers pass keys: `ChunkGroupKey::{Entry, Async, Isolated, Shared}`,
or the `.key()` of a graph-derived merged group.
### Why?
Chunking already resolved every group by key and failed if the group
wasn't registered, so the entries a caller passed in were redundant at
best and inconsistent with the graph at worst. With key-only APIs,
`ChunkGroupInfo` is the single source of a group's contents, and callers
can no longer construct a group that disagrees with the module graph.
This is PR 4 of a six-PR stack, based on `canary` (#98733, #99686 and
#99749 are already merged):
1. #98823 — Pages Router dev client graph chaining
2. #99910 — regression test for Pages Router `_app` availability
(#99789)
3. #98734 — use graph-derived merged groups
4. **This PR** — chunking takes a `ChunkGroupKey`
5. #98685 — chunk-group availability bitmaps
6. #99050 — keyed chunk-group index lookups
### Verification
- The tree of this PR is identical to the previous #98734 head, which
passed:
- Workspace `cargo clippy --all-targets -D warnings`, per-layer `cargo
fmt` and workspace check
- Turbopack snapshots: 132 passed, no snapshot changes
- Turbopack production and dev `404-page-router`,
`app-dir/actions/app-action` and `app-dir/initial-css-order`; production
`edge-pages-support`, `app-dir/css-order` and
`layout-segment-chunk-sharing`
<!-- NEXT_JS_LLM -->
<!-- fleet 4f67c6f7-bbe2-454b-9a17-822a60ef8f7e -->
Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
sokra
added a commit
that referenced
this pull request
Oct 24, 2026
### What? Replaces Turbopack's retained available-module sets with compact chunk-group bitmaps. This is PR 5 of a six-PR stack, based on `canary` (#98733, #99686 and #99749 are already merged): 1. #98823 — Pages Router dev client graph chaining 2. #99910 — regression test for Pages Router `_app` availability (#99789) 3. #98734 — use graph-derived merged groups 4. #99750 — chunking takes a `ChunkGroupKey` 5. **This PR** — chunk-group availability bitmaps 6. #99050 — keyed chunk-group index lookups ### Why? `AvailabilityInfo` previously retained every concrete module, module batch, and async-loader item emitted by ancestor chunk groups. The same module lists were duplicated across availability chains, increasing memory use for large graphs. The module graph already records which chunk groups contain each module. Retaining only available group indices lets availability checks reuse that information. ### How? - Stores available chunk-group indices in a roaring bitmap (`AvailableChunkGroups`), replacing `AvailableModules`. - Determines module availability by intersecting its `ChunkGroupInfo` membership bitmap with the available groups. - Uses a batch's representative first module, because every module in a batch has identical group membership. - Tracks async-loader membership by the parent groups that emit each loader, preserving nested loader deduplication. - Scopes availability to its `ModuleGraph`: chunk-group indices only mean something inside one graph's `ChunkGroupInfo`, so the graph is stored alongside the bitmap and asserted on every use, and `translate` re-expresses availability in another graph by mapping indices through their canonical `ChunkGroupKey`s. Groups the target graph doesn't have are dropped, which can only duplicate a module, never omit one. Behavior is unchanged. Availability-dependent asset identifiers are derived from the *identity* of the available chunk groups — each group rendered via `ChunkGroupKey::ident_str` (module idents plus merge tags, merged parents resolved recursively) and sorted — never from the raw graph-local indices, which are only meaningful within a single `ModuleGraph` and would collide across the per-endpoint graphs used in development. Deterministic filenames change as a result; the snapshots are updated accordingly and the module IDs they contain are identical. One observable improvement: in Turbopack dev, `/_app` and the page are chunked in the same graph (#98823), and availability is now tracked by chunk-group membership, so modules shared by both are loaded only once. The `// @gate !(turbopack && dev)` that #99910 put on the "loads modules shared by _app and the page only once" assertion in `app-document-import-order` is therefore stale, and this PR removes it. ### Verification - `turbopack-core` — 164 passed - `next-core` — 84 passed - Turbopack snapshots — 126 passed - Nested async availability coverage verifies shared-module and loader deduplication - Pages/App development and production suites, including `404-page-router`, `server-navigation-error`, client-reference chunking, and full `css-order` - Turbopack dev `app-document-import-order` (now ungated) and `pages-app-availability-boundary`, on this PR and on #99050 - Affected Cargo checks and Rust formatting ### Benchmarks Measured on commits `64b73c08` (this PR) against `c543a06d` (#98823) — the pre-rebase SHAs of this stack. The stack has since been rebased onto newer canary; the refactor's code diff is byte-identical across that rebase, so these numbers still characterise this patch, but they were not re-measured on the current heads. The two commits were one apart and differed only in Rust, so the JS build was shared and the two native binaries were swapped in place with their sha256 asserted before every run. `next build --turbopack`, cold (`rm -rf .next`) every run, **3 runs per side, sides interleaved**. Memory is the peak **aggregate RSS of the whole build process tree**, sampled from `/proc` every 50 ms (GNU `time -v` reports only the max of a single waited-for child, which understates a multi-process build). Median shown, with min–max in brackets. "disjoint" = the two sides' ranges do not overlap at all. **Workloads where availability actually engages** — it only does work when a chunk group inherits availability from a parent, i.e. dynamic imports or multiple entries: | Workload | Metric | base | this PR | Δ | | |---|---|---|---|---|---| | 200 routes × 6 dynamic imports | build | 108.7 s [105.7–111.6] | **75.9 s** [74.0–78.0] | **−30.1%** | disjoint | | | peak RSS | 3462 MB [3361–3467] | **3093 MB** [3052–3153] | **−10.7%** | disjoint | | 750 routes, no dynamic imports | build | 314.5 s [313.1–323.0] | **292.4 s** [289.4–311.2] | **−7.0%** | disjoint | | | peak RSS | 9220 MB [9212–9687] | 10904 MB [9196–11164] | — | inconclusive | **Control workloads** — single entry, no `import()`, so availability is effectively root everywhere and there is nothing to improve: | Workload | build Δ | peak RSS Δ | | |---|---|---|---| | `bench/module-cost` (~18.6k static modules) | −3.3% | 0.0% | noise | | `bench/heavy-npm-deps` (1 page, heavy deps) | −1.9% | −1.6% | noise | Build time improves monotonically with how much availability a workload exercises (−1.9%, −3.3%, −7.0%, −30.1%). Emitted `.js` file counts are identical between sides on every workload, consistent with unchanged behaviour. Caveats: the dynamic-import workload is synthetic and its 30% figure is not a typical-app claim; n=3 with no statistical test; aggregate RSS double-counts pages shared between processes; the 750-route memory result is bimodal on this PR's side (its own range spans the base median) and is therefore reported as inconclusive rather than a regression. The many-pages fixture was reduced from its stock 3000 routes to 750 because 3000 exceeds the 16 GB benchmark machine and thrashes, which would measure swap rather than chunking. <!-- NEXT_JS_LLM --> <!-- fleet 4f67c6f7-bbe2-454b-9a17-822a60ef8f7e --> Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com> Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What?
Narrows the turbo-tasks dependency of
ChunkGroupInfo::get_index_ofso that looking up one chunk group's index no longer depends on the entireChunkGroupInfo.This is PR 6 of a six-PR stack, based on
canary(#98733, #99686 and #99749 are already merged):_appavailability (Turbopack dev (16.4.0): Pages Router page modules missing ("module factory is not available") after _app availability seeding (#97664) #99789)ChunkGroupKeyWhy?
#98685 makes availability a set of chunk-group indices, which turns
get_index_ofinto a hot path: every availability translation between graphs resolves keys to indices through it.That lookup previously read
chunk_group_keysoff the wholeChunkGroupInfovalue. In turbo-tasks, reading one field of a cell creates a dependency on the cell as a whole, so every index lookup in the graph was invalidated whenever any unrelated part of the chunk group info changed — a new async chunk group anywhere in the app re-ran every lookup, and everything downstream of them. The wider the availability work built on top of it, the more that costs.How?
Adds
ChunkGroupKeyToIndex, acell = "keyed"map fromChunkGroupKeyto its index, alongside the existing set, exposed through an argument-lesschunk_group_indices()task.get_index_ofreads a single key from it throughVc::get, so its caller depends only on the key it actually asked for: an unrelated chunk group changing no longer invalidates it.get_index_ofitself is deliberately a plain async method rather than a turbo-tasks function. As a function it would create one task per chunk group argument, which is exactly the per-chunk-group overhead this change is meant to remove; the singlechunk_group_indices()task is what provides the caching.The interesting constraint is why this can't reuse the
FxIndexSet<ChunkGroupKey>that was already there — a set that already holds exactly these keys in exactly this order. Two properties of the keyed-cell traits rule it out:KeyedAccess for IndexSetyields(), not the index, so the value needed isn't retrievable through a keyed read.KeyedEq for IndexSetcompares membership and ignores position. A key whose index merely shifted — same set, different order — would not be reported as changed, and lookups would keep returning a stale index. That is a correctness bug, not just a missed invalidation.Mapping the key to the index as the value makes
KeyedEq for HashMapcompare indices, which detects exactly that shift. TheFxIndexSetstays for the reverse (index → key) direction, so the duplication is deliberate and documented at the type.The debug-only "couldn't find chunk group" diagnostic still reads the whole struct to render its message. That path is only taken on failure, so the happy path keeps its narrow dependency.
Verification
Keyed-cell invalidation semantics (per-key dependencies,
KeyedEqcomparing values) are covered by turbo-tasks' own tests, so this PR does not add a dedicated invalidation test.cargo clippy --workspace --all-targets -- -D warnings -A deprecatedturbopack-core+next-core— 252 passedcargo fmt --checkcleanBehavior is unchanged; this is a dependency-graph refactor with no effect on emitted output.