Turbopack: stop copying sourcesContent into every serialized source map (#95934)
This reduces peak memory consumption with the original repro case in
[vercel/next.js#81161](https://github.com/vercel/next.js/issues/81161).
If you walk the 15 routes in a quick succession and capture vmmap
--summary at the end of each fast sweep (before memory is released),
with this change the number reliably goes down from 3 GB to 2.6 GB:
```
before 1 3.0G 17
after 1 2.6G 16
before 2 3.0G 16
after 2 2.6G 16
before 3 3.1G 18
after 3 2.6G 17
before 4 3.1G 17
after 4 2.6G 16
before 5 3.1G 17
after 5 2.6G 17
before 6 3.0G 17
after 6 2.5G 17
before 7 3.1G 17
after 7 2.6G 17
before 8 3.0G 25
after 8 2.6G 24
before: [3.0, 3.0, 3.1, 3.1, 3.1, 3.0, 3.1, 3.0]
after: [2.6, 2.6, 2.6, 2.6, 2.6, 2.5, 2.6, 2.6]
mean before=3.050G after=2.587G diff=0.462G (-15.2%)
exact permutation test: p = 2/12870 = 0.00016
```
More runs on this app:
<img width="938" height="623" alt="Screenshot 2026-07-22 at 20 59 12"
src="https://github.com/user-attachments/assets/01c41656-eb5a-41f9-8871-b4ce262c54d5"
/>
And here's an internal app showing ~20% dev memory win at peak (although
reclaiming memory settles it):
<img width="832" height="402" alt="Screenshot 2026-07-22 at 16 45 34"
src="https://github.com/user-attachments/assets/8c39f668-d431-43f9-b533-d39dfaa97c15"
/>
---
<details>
Module source maps are serialized to a JSON rope right after code
generation and treated as opaque bytes from then on. Since
`sourcesContent` — the full original source text — is inlined into that
JSON, every later step that modifies the map has to copy all of it:
rewriting the `sources` URLs for each chunking context copies the entire
map per context, and embedding the module map into a chunk's sectioned
map copies it again, once per layer that includes the module. In a dev
session on a dependency-heavy app, each module's source text ends up
resident ~5 times over, and source maps end up dominating the memory the
task store holds for codegen output (in one measured app, maps were 57%
of module factory bytes, and `sourcesContent` alone was 36%).
This PR stops copying the text. `StructuredSourceMap` keeps a map in
field form until the moment it is actually emitted:
- Fields we never touch (`mappings`, `names`, `version`, ...) are stored
as verbatim raw JSON snippets, so producers' bytes round-trip untouched
and re-emission is rope sharing rather than re-serialization.
- `sources` is the one field rewrites modify, so it is stored typed, and
the per-chunking-context "fileify" rewrites become a plain string
rewrite of that field instead of a parse–modify–reserialize of the whole
map.
- `sourcesContent` entries are individual, already-JSON-escaped ropes.
Rewritten maps and chunk maps share them; the source text is escaped
once per module and never copied again.
Two construction paths, chosen by where the bytes come from: maps we
generate ourselves are built directly from the `swc_sourcemap` object
(`from_swc_map` takes the contents out, serializes the small remainder,
and never round-trips source text through JSON), while maps that arrive
as bytes (prebuilt maps next to external code, the
single-complete-original fast path) go through `from_json`. The emitter
writes fields in exactly `swc_sourcemap`'s order and format, pinned by
golden tests that assert byte-identity with `to_writer` — so emitted
chunks and maps are byte-for-byte unchanged, and no snapshot fixtures
change. The CSS pipeline gets the same treatment; the single-file output
path is left alone since it serializes straight into the final artifact
and there is nothing to share.
The win is proportional to how much duplicated source-map text an app
holds, so results range accordingly. On a dependency-heavy reproduction
app (13 routes sharing large wallet SDKs; rapid route sweep in `next
dev`, macOS physical footprint, 8 interleaved cold runs per side): 3.05
GB → 2.59 GB mean (−15%), with zero overlap between the two
distributions (exact permutation test p ≈ 0.00016) and per-route
latencies unchanged. On `bench/basic-app` (small first-party modules): a
full sweep of all 42 routes drops the post-sweep footprint 3.4–3.5 GB →
2.4–2.5 GB (−29%, interleaved runs); cold compile −4%, idle footprint
identical (eviction reclaims both sides equally at idle). On
`bench/heavy-npm-deps` (heavy deps disabled in its default config) and
on very large first-party apps whose memory is dominated by the JS heap
and application state rather than map text, the change measures flat —
as expected, since there is little duplicated text for sharing to
recover. `small_apps` build benchmarks are flat except `date-fns-all` at
+1–2% (~2.5 ms): that app is 1000+ tiny modules and `from_swc_map` adds
one small-skeleton JSON parse per module, with no redundancy to win back
in a single-context one-shot build. The single-complete-original fast
path (per prebuilt module with a shipped map, and per minified chunk) is
cost-neutral: the structured map is built by splitting the raw map's
serde fields directly (`from_serialize`), so the serialized document is
never materialized or re-parsed — 93 ms per 5 iterations on a synthetic
21.7 MB map, matching the pre-PR baseline exactly, with byte-identical
output pinned by a property test. Serialized task-store size is
unchanged (bincode expands sharing back out), so this is purely a
live-memory change.
Verified: snapshot suite passes with zero fixture changes, execution
suite matches canary (the single pre-existing failure reproduces on the
base tag), unit suites pass including new golden byte-identity tests,
and emitted `.map` files were validated end-to-end in a running dev
server (multi-section chunk maps, fileified `file://` sources,
`sourcesContent` present and byte-identical between base and this
branch).
</details>