Turbopack: write smaller/faster client reference manifests (#101312)
## Summary
Turbopack's `*_client-reference-manifest.js` files repeat the same chunk
lists for every client reference: each `clientModules`,
`ssrModuleMapping`, and `rscModuleMapping` entry of a page lists all the
chunks of its chunk group, so a manifest is mostly copies of a handful
of lists. On a large app the manifests totaled **1.67 GiB** on disk,
were the single largest retained allocation site in the build (~1.7 GiB
of the live heap at the end of `next build`), and took ~6 s to evaluate
at runtime (`eval` in node).
This writes the manifest as a JS expression that evaluates to the same
object, but:
- writes each distinct chunk path once, in a string table `s`, with
directory prefixes that repeat shared as constants (`p0+"abc.js"`)
- writes each chunk list that is used more than once once, in a table
`l`
- encodes lists as runs of consecutive string indices,
`r(start,length,...)`, since paths are numbered in first-seen order
- builds entries with factories, `e(id,chunks)` / `n(id,chunks)`,
instead of repeating `{"id":..,"name":"*","chunks":..,"async":false}`
- does the same for `entryJSFiles` and `entryCSSFiles` (inlined CSS
contents go in the string table too, without prefix splitting)
```js
(() => {
const p0 = "/_next/static/chunks/", p1 = "server/chunks/ssr/", p2 = "static/chunks/";
const s = [p0+"…js", …];
const r = (...a) => { /* s[a[0]..a[0]+a[1]], … */ };
const l = [r(0,46), …];
const e = (id, chunks, async = false) => ({ id, name: "*", chunks, async }), n = …;
(globalThis.__RSC_MANIFEST ??= {})["/page"] = {"moduleLoading":…,"clientModules":{"[project]/…":e(2501066,l[0]),…},"ssrModuleMapping":{"2501066":n(5564085,l[3]),…},…};
})();
```
The tables are built while the manifest is built (`ChunkTables` in
`next_manifests/chunk_tables.rs`) instead of serializing to JSON first.
Lists that occur several times now evaluate to the same array object;
nothing mutates them (the runtime deployment-id suffix maps them into
new arrays).
Other changes:
- `next internal static-routes-info` `require()`s the manifest and reads
the global instead of extracting a JSON body by hand, since the body is
no longer JSON.
- Chunk URLs are percent-encoded with `encode_uri_path`, which writes
into the URL buffer directly and does not allocate when nothing needs
escaping.
### Results (a large app, `next build
--experimental-build-mode=compile`, cold)
| | before | after |
| --- | --- | --- |
| Client reference manifests on disk (939 files) | 1674.7 MiB | 34.0 MiB
|
| Evaluating all manifests (`node` 24, `runInThisContext`) | ~6.0 s |
~0.5 s |
| Heap retained by the loaded manifests | 313 MiB | 121 MiB |
| Live heap at the end of the build (sampled) | 14.67 GiB | 12.91 GiB |
| Build time | 79.4 s | 79.0 s |
Fewer array and object literals also make the scripts faster to parse
and compile in V8, which is why evaluation improves more than size alone
would suggest.
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>