### What?
Bound the compile-time Google Fonts fetch on the Turbopack path so `next
dev` / `next build` can't hang forever on a stuck network.
### Why?
`next/font/google` fetches from Google Fonts at compile time. When the
connection *hangs* (captive portal, packet-dropping proxy, broken IPv6),
the fetch had no timeout, so compilation blocked until the OS connect
timeout (~75s) or indefinitely.
### How?
- `FetchClientConfig` gets a `connect_timeout` (10s) and total `timeout`
(60s); Google Fonts overrides them to 5s / 30s.
- Transient failures retry up to 3×, each attempt its own
`duration_span!`.
- On failure: `next build` errors (fails the build), `next dev` warns
and uses the fallback font. Both messages report the attempt count and
suggest `next/font/local`.
### Follow-ups
- [ ] Reduce the dev-mode timeout (build keeps the longer value).
- [ ] Port the fix to the webpack/JS path (`packages/font/src/google`).
Related: #92301. #76473.
---------
Co-authored-by: Benjamin Woodruff <benjamin.woodruff@vercel.com>
The `rewriteTurbopackSources` function has been mostly dead since #85146
this closes the remaining gap with the edge runtime in dev (an
accidental omission) and drops the js side munging
Noticed this bug where we were assuming chunks were plain strings, which
may not always be the case given #95261. When putting together
https://github.com/react/react/pull/37095, I noticed this was also a
spot we were missing so this fixes that.
Almost all of this diff is a regression test.
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>
I was seeing failures on https://github.com/vercel/next.js/pull/95628
and went down this rabbit hole.
- We were (incorrectly) not canonicalizing the `DiskFileSystem` root
dir. This happened to work out for us because pnpm also wasn't
canonicalizing junction point targets, and so the root dir prefix
matched. However after `#95628`, pnpm started canonicalizing junction
point targets because we added `pnpm-workspace.yaml` files. This was
just coincidence and indicative of much deeper problems.
- `dunce` isn't a good fit for us. It picks a win32 or verbatim
representation based on path length, which would mean that long paths
inside of a short root path would never work because we'd never be able
to strip the prefix, since we'd be looking for a win32 path prefix.
Omnipath is better for what we want to do:
https://docs.rs/omnipath/latest/omnipath/windows/trait.WinPathExt.html
- The windows verbatim path format works with long paths (>260
characters) and behaves a lot closer to cross-platform unix paths, which
is what we want. E.g. if somebody tries to import a JS module from a
Windows 8.3 short path, it should fail. This will fail with verbatim
paths (good!). This PR *always* uses the verbatim path format internally
within `DiskFileSystem`.
- `read_link` was re-inventing `try_from_sys_path` but badly. Just call
`try_from_sys_path`.
- `turbopack/crates/turbo-tasks-fs/src/embed/file.rs` was dead code.
- `validate_path_length_inner` was incorrectly referring to verbatim
paths as UNC paths. These are two different things.
- Canonicalize any absolute symlink targets outside of the
`DiskFileSystem` root path, resolving any symlinks they may have,
normalizing case-insensitive paths, and handling Windows 8.3 short name
format.
## Some Context About Windows Path Formats:
- win32 paths: These are your normal user-friendly `C:\foo\blah` paths.
When calling most filesystem APIs, these are limited to 260 characters
unless the user has enabled long paths on the system, and avoiding the
limit with these paths also requires a [`longPathAware` application
manifest](https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests#longPathAware).
You can mix `/` and `\` in win32 paths, and Windows will normalize the
path separator for you.
- verbatim paths: This is a rust-specific name (used by the stdlib) for
the "extended length" prefixed paths. These are paths starting with
`\\?\`. These paths can be up to ~32,767 characters long. They do not
support Windows 8.3 shortnames. They do not normalize between `/` and
`\`, and do not support `.` or `..` components, however
[`Path::join`](https://doc.rust-lang.org/std/path/struct.Path.html#method.join)
will do this normalization if given a verbatim path.
- Windows 8.3 shortnames: A legacy format where paths can be represented
by 8 characters, followed by a tilde and a digit (or hash in some
extreme cases where there are many collisions), followed by a 3
character file extension. NTFS supports this,
[ReFS](https://learn.microsoft.com/en-us/windows-server/storage/refs/refs-overview)
(used by [Windows Dev
Drive](https://learn.microsoft.com/en-us/windows/dev-drive/)) does not.
- UNC ("Universal Naming Convention"): This is a specific representation
of paths used for remote files on network servers.
- `file://` URIs: These must be formed from [win32 style
paths](https://docs.rs/url/latest/url/struct.Url.html#method.from_file_path),
though the normal 260 character limit typically does not apply here.
- canonicalize: Corresponds to
[`std::fs::canonicalize`](https://doc.rust-lang.org/stable/std/fs/fn.canonicalize.html).
This involves actual fs operations (that we can't easily track), and
recursively resolves symlinks. It will fail if the file path does not
exist. On windows, this always returns a verbatim path.
- wide format: Windows paths are utf-16. Generally we don't support
paths that can't be encoded to utf-8, but we should try to fail as
obviously as we can in these cases. We don't have good filesystem issue
handling yet though.
- case sensitivity: Generally paths are not case-sensitive on Windows or
macOS, but are case-sensitive on most Linux filesystems. The exact
definition of how case sensitivity works with unicode paths is highly
platform and filesystem dependent.
- junction points: The windows equivalent of symlinks (symlinks are also
supported, but require developer mode to be turned on). These can
contain an arbitrary path reference in any format (win32, verbatim, etc)
with any case and with windows 8.3 short names. Junction points are
always target absolute paths to directories.
This has been causing a lot of confusion when contributors have been
debugging tree shaking, eg.
https://github.com/vercel/next.js/issues/95698 and the linked issues.
Turbopack's standard tree-shaking features are controlled through the
`turbopackRemoveUnusedExports` and `turbopackRemoveUnusedImports`
options which default to true.
This PR introduces two new things: a pass of the analyzer that
recognises exports from CJS modules (in the form of `exports.foo`, see
`analyze_cjs_exports`) and then a CodeGen that reads `export_usage_info`
to remove CJS exports that are not used.
This PR does not add anything additionally to `export_usage_info`, so
this means that we only trim from modules that are imported with `import
{ foo } from "bar"`. The CodeGen depends on the output of
`analyze_cjs_exports` for the locations of exports, whether it is safe
to drop exports (its pretty conservative), and also whether `__esModule`
is present.
This PR builds on top of the other PRs in this stack, it is the PR that
switches to inlining the bootstrap code and dropping the runtime file
that each route had.
We used to generate the runtime file for each `ChunkGroupResult` in
`evaluated_chunk_group()`, I've added `chunk_group_bootstrap_params`
which are the parameters that were unique to each chunk group. These are
inlined into each page's HTML in
https://github.com/vercel/next.js/pull/94666. They look like:
```javascript
{"otherChunks":["output/0_9x_turbopack-tests_tests_snapshot_basic-tree-shake_export-namespace_input_1mqgz2-._.js"],
"runtimeModuleIds":["[project]/turbopack/crates/turbopack-tests/tests/snapshot/basic-tree-shake/export-namespace/input/index.js [test] (ecmascript)"]}
```
Whether or not the inline-able code is generated is based on the
`inline_chunk_group_bootstrap` which is still false in all cases at the
moment but will be turned to true in
https://github.com/vercel/next.js/pull/94666.
#95815 refactored the NFT flow and ended up only tracing the RSC
template and it's subgraph. Not the server actions loader module whose
module graph is completely separate.
---
Clauded.
Attempts to fix
https://github.com/vercel/next.js/discussions/95130#discussioncomment-17652155.
---
<details>
## Summary
Fixes the NFT tracing regression behind #95815 and the deploy failures
reported in
https://github.com/vercel/next.js/issues/87737#issuecomment-4897366831.
Since the 16.3 NFT rewrite (#94224 / #92901), a `serverExternalPackages`
entry that is imported **only from a `'use server'` action referenced by
a client component** ends up in the route's trace as the content-hashed
alias symlink (`.next/node_modules/<pkg>-<hash>`) **without the store
files the symlink points to**. Any output assembled from the traces
(Vercel lambdas, `output: 'standalone'`) then contains a dangling
symlink, and invoking the action throws `Failed to load external module
<pkg>-<hash>` (HTTP 500). 16.2.9 traces both. Both CJS and ESM externals
are affected, with any package manager — the ESM/bun/monorepo framing in
the issue reports was a coincidence of which packages were only
reachable through actions.
The cause: `AppEndpoint::trace_result` started the trace DFS only from
`rsc_entry`, but the server actions loader is a separate module-graph
entry (`additional_entries`). It is chunked into the endpoint output
(which emits the alias symlink via the chunk's output assets), yet its
subgraph — including the externals' traced target files — was never
visited by the tracer. `trace_endpoint` now accepts multiple entry
modules and the app endpoint passes the actions loader alongside
`rsc_entry`.
The test observes the user-visible failure: it builds with `output:
'standalone'`, deletes everything except the standalone output (so only
traced files are available, like a deployed lambda), runs `server.js`,
and invokes the server action in a browser. Without the fix the action
fails with `Failed to load external module lodash-<hash>`; on 16.2.9 the
same fixture responds with the action result. Note the fixture must not
import the externals from any other route: the standalone output is the
union of all route traces, so a route that traces them correctly would
mask the missing entries.
## Verification
- `pnpm test-start-turbo
test/production/standalone-mode/server-action-externals/standalone-mode-server-action-externals.test.ts`
— fails without the fix (`Failed to load external module
lodash-fb78ad0536ffcdab` from the standalone server), passes with it.
The equivalent standalone flow on `next@16.2.9` returns the action
result with HTTP 200.
- `pnpm test-start-turbo
test/e2e/externals-transitive/externals-transitive.test.ts`
- `pnpm test-start-turbo
test/production/standalone-mode/server-actions/standalone-mode-server-actions.test.ts`
<!-- NEXT_JS_LLM -->
</details>
Extracted from https://github.com/vercel/next.js/pull/95468 and arguably
the main fix there.
We need to add code to wait for top-level awaits in two cases:
- If a metadata module (like `opengraph-image.tsx`) is slow to evaluate,
cache warming wouldn't "wait" for it, and then the actual build would
fail with `Unexpected cache miss after cache warming phase`. This is
fixed with placing `instrumentModuleGetter` around them.
- If the evaluation is fast but still async (e.g. `await readFile` on a
large font), the build passes but the segment collector reads the module
too early, doesn't see `generateStaticParams`, and so the image becomes
dynamic instead of prerendered. The fix is `await ensureUserland` before
looking at exports.
Remaining callsites are wrapped so we don't add more bugs like this, but
it's not strictly required now because they are already being awaited
elsewhere. I've done this for consistency.
New tests fail before the fix.
Fixes#95125 in two ways
The OP admits that they misread the message in a few ways, but we can
fix all of them
1. Be specific about what function might need an annotation in the error
message. A user with a nearby `path.join` might think that that is where
the annotation belongs instead of on the function with the warning.
2. Be more flexible with how these `ignore` comments are interpreted
`fs.readFileSync(path.join(process.cwd(), someVar))` shouldn't require 2
annotations. Handle this by checking for and detecting `turbopackIgnore`
comments in all expressions! This will then bubble up through the linker
and just cause us to ignore corresponding fs access apis.
## Summary
The graph-based CSS chunker's `linearize` step produces the global
module order that `split_into_chunks` then cuts into chunks. It
previously broke ties between ready modules using a weight-sorted stack.
It now prefers the ready module that shares the most chunk groups with
the previously placed module, so modules that are loaded together end up
adjacent in the global order and split into better-aligned chunks. On a
real world example this measurably lowers the modeled chunk-loading
cost, with fewer requests and less over-fetched CSS.
The shared-group count is read from edge weights (how many chunk groups
two modules co-occur in). Since `make_acyclic` deletes edges to break
cycles, it now returns the edges it cut so `linearize` can still count
those conflicting-order co-occurrences — reconstructing the full
co-occurrence without cloning the graph.
## Verification
- `cargo test -p turbopack-core style_groups_graph::tests` (all pass)
- Validated on a real world example that the resulting global order is
deterministic and the modeled chunk-loading cost decreases.
<!-- NEXT_JS_LLM_PR -->
Ensures the module graph is not bundled. Right now this has no effect
since all the modules imported by `navigation-test-lock` have legit uses
in prod builds.
Also nice to not have to fork every single implementation inline.
### What?
Adds a new `'auto'` mode to the experimental `turbopackMemoryEviction`
config option and makes it the default. In `'auto'` mode, Turbopack only
evicts in-memory cache after a snapshot once enough memory has been
allocated since the last eviction to make the work worthwhile.
The option now accepts three values:
- `false`: never evict.
- `'auto'` (new default): evict after a snapshot only once a memory
threshold has been crossed.
- `'full'`: evict all evictable data after every snapshot (the previous
behavior).
### Why?
When the persistent (FileSystem) cache is enabled, Turbopack snapshots
its in-memory state to disk and can then evict those in-memory copies to
reclaim memory, reloading them from disk on demand.
Previously, with eviction enabled (`'full'`), we evicted after *every*
snapshot. This is too aggressive: tasks get restored from disk and then
immediately evicted again, cycle after cycle, wasting work for little
memory benefit.
`'auto'` mirrors the existing persistence-threshold model (we already
skip a snapshot when too little compilation time has accumulated to
justify its cost). Here the proxy is memory instead of time: it isn't
worth paying the restore-then-re-evict churn to reclaim a small amount
of memory.
This was motivated by an example in v0.app where the client would poll
the server every 5 seconds leading to a pathological behavior
* client poll ->
* next.js ensurePage ->
* turbopack writeEndpointToDisk
* recompute and restore settings for that endpoint (3-10ms of io work)
* realize there is nothing to do
* respond to client
* 2 seconds later..... persist and evict everything saving 0.5M of ram
(100ms of work! writes out 2 SSTs and then compacts them!)
* 3 seconds after that restart the loop
This is silly, #95137 will prevent the persistence loop from occurring ,
this PR will also just skip the 'restore and recompute' work using a
similar strategy.
### How?
We can't measure exactly how much memory a sweep would reclaim, so we
use `TurboMalloc::memory_usage()` (process-global net live bytes) as a
proxy. In `'auto'` mode an eviction sweep runs only once the net bytes
allocated since the last eviction exceed a threshold (default 128 MiB,
overridable via `TURBO_ENGINE_EVICT_MIN_BYTES`). The first eviction
after startup always runs. The threshold scales down under OS memory
pressure (`TurboMalloc::memory_pressure()`) so we evict more eagerly
when memory is tight.
**Backend (`turbo-tasks-backend`):**
- `BackendOptions.evict_after_snapshot: bool` → `eviction_mode:
EvictionMode` (`Off` / `Full` / `Auto`).
- New `EvictionControl` type owns the policy: the mode plus the
threshold bookkeeping. The background snapshot loop calls
`should_evict(snapshot_had_new_data)` once per cycle and
`record_eviction()` after a sweep, so the loop no longer branches on the
mode. `'full'` and `false` behave exactly as before.
<!-- NEXT_JS_LLM_PR -->
I'm introducing a new endpoint that doesn't compile SSR when doing a
page navigation, I'd like to call it a `rscEndpoint` but currently this
name is taken. `rscEndpoint` appears to be used for detecting changes
for HMR so I've renamed it to `rscHmrEndpoint` so I can use that name
for the new `rscEndpoint`.
Thus far, it wasn't possible to use `require()` in a template file with
Webpack.
But I needed to add a `if(NEXT_RUNTIME !== "edge") { const x =
require("../../something"); }`
Turbopack doesn't do any regex rewrite hacks, so this just worked
Instead of hardcoding the list, we now automatically detect any
`.browser` sibling and create a compiler alias for that module.
Together with a linting rule, we'll ensure no future regressions are
handled automatically (detection of conditional require and missing
.browser sibling).
This isn't the smoothest DX yet. Ideally we'd generate the list at
Next.js compile time (i.e. before publishing) to avoid having to rerun a
script (I'll follow-up). Generating the list during build time (next dev
or next build) is just another tiny cost users would have to pay.
### What?
When profiling is enabled (`--experimental-cpu-prof` /
`--internal-trace`, or the `NEXT_CPU_PROF` / `NEXT_TURBOPACK_TRACING`
env vars), Next.js writes a `.gitignore` containing `*` into
`.next-profiles/` as it creates the directory. This keeps the profiling
output out of git and out of the way of tools that scan the project
tree.
### Why?
`.next-profiles` is a sibling of `.next`, so it's intentionally *not*
covered by the `.gitignore` create-next-app ships. The motivation was
that profiles wouldn't bloat `.next` and wouldn't be silently ignored.
But a sibling dir full of large profile/trace files turns out to be a
liability:
- **Ecosystem tools scan it.** Tailwind v4 aggressively auto-scans files
and has opened a `trace-turbopack.bin` and crashed; Turborepo <2.8.13
hashes the file and OOMs, and >=2.8.13 fails to start `dev` watchers
because reading the large trace is too slow. (workflow watchers are
likely affected too.)
- **Turbopack NFT globbing** can read/hash the file via overly broad
`fs.read` patterns.
- **Accidental commits.** Pushing a multi-GB trace would be miserable
for a customer helping us debug.
These have each gotten point bandaids (#94587, #94570), but the real fix
preserves the original motivation: a nested `.gitignore` with `*` keeps
the files off git **and** makes gitignore-respecting tools (Tailwind,
Turborepo) skip the whole subtree, which resolves the scanning failures.
Reverting to `.next/profiles` was the alternative; the gitignore
approach keeps the existing path.
### How?
- `ensureProfilesDir()` (`packages/next/src/lib/profiles-dir.ts`)
creates `.next-profiles` and writes the `.gitignore` (only if absent, so
user edits survive). Single source of truth.
- `bin/next.ts` calls a `setupProfilesDir()` helper on build/dev/start,
gated on the env vars so both the CLI flags and direct env-var usage are
covered. Skipped when `NEXT_TURBOPACK_TRACING_PATH` redirects trace
output.
- Rust keeps `create_dir_all` as a backstop for the trace dir; the
`.gitignore` is written in JS only.
- Docs switched from `NEXT_TURBOPACK_TRACING=1` to `next dev
--internal-trace`, and the stale `trace-turbopack` filename corrected to
`trace-turbopack.bin`.
<!-- NEXT_JS_LLM_PR -->
This computes the implementation hash of a given `use cache`:
- it includes the module containing the `use cache` function, plus all transitive imports of that module (including externals via NFT)
- what does it hash
- for bundled code, it hashes the generated code (so including any AST transforms, inlined env vars, etc)
- for NFTd files, it hashes the file content on disk
- So this is basically equivalent to hashing the generated JS chunks, except that it's scoped to only what the given actions module uses
- (so notably, if you have multiple use cache functions in a given file, then they will all have the same hash)
<br/>
- [x] Gate behind experimental flag
- [x] Only compute hash for `use cache` functions, not all server actions
- [ ] `final_read_hint` is a problem. Some chunk items are now codgened twice, so it recomputes the AST
- [x] disable for dev for now
- [ ] Measure perf impact
```jsonc
// .next/server/server-reference-manifest.json
{
"node": {
"806f4954cfbb75404a19d6d405065ed9059cc0cab2": {
"workers": {
"app/rsc/page": {
"moduleId": "[project]/bench/app-router-server/.next-internal/server/app/rsc/page/actions.js { ACTIONS_MODULE0 => \"[project]/bench/app-router-server/app/rsc/logic.js [app-rsc] (ecmascript)\" } [app-rsc] (server actions loader, ecmascript)",
"async": false,
"exportedName": "$$RSC_SERVER_CACHE_0",
"filename": "bench/app-router-server/app/rsc/logic.js",
"codeHash": "413e394d9597b35df12828a6a7fd6363" // <------------
}
},
"filename": "bench/app-router-server/app/rsc/logic.js",
"exportedName": "$$RSC_SERVER_CACHE_0"
}
},
"edge": {},
"encryptionKey": "jOF2TbpNLjp2oOhl3VPBwGE7luodnei9clJPq/gaYQo="
}
```
Under Cache Components, a dynamic route whose `generateStaticParams`
returns an empty array is intentionally an error, but it surfaced badly:
in development the request failed with an uncaught error that left a
blank screen and a 500, and a production build printed the error without
a stack. The cause was that `throwEmptyGenerateStaticParamsError`
deliberately discarded the stack, because the throw happens in framework
code in `buildAppStaticPaths` after the user's function has already
returned, so there was no user frame to point at.
This change adds an SWC transform that gives the error a stack anchored
at the user's code. For a `page`, `layout`, or `default` file that
exports `generateStaticParams`, it emits a
`__next_create_empty_gsp_error` factory whose `new Error` is span-mapped
back to the source. It keys off the export rather than any declaration
named `generateStaticParams`, so it covers `export function`/`export
const`, `export { x as generateStaticParams }`, and `export {
generateStaticParams } from '...'`, while ignoring a same-named local
helper that is never exported. The anchor is the most specific
available: the `return []` literal when it is unambiguously the
function's only return, the declaration, or the export statement when
the body lives in another module. The transform is registered for both
bundlers, gated on Cache Components and excluded from the edge runtime,
mirroring the existing `debug_instant_stack` wiring.
At runtime the factory is attached to the segment as
`createEmptyParamsError` in `collectSegments`, and `buildAppStaticPaths`
throws it when it detects an empty result. Because the error now
originates in the page module rather than framework code, development
surfaces a proper redbox with a meaningful stack and production fails
the build with a stacked CLI error. The custom
`EmptyGenerateStaticParamsError` name has been dropped since it only
added noise to the logs. A few export forms, notably a wildcard `export
*` re-export, can't be detected from the page module alone, so when the
factory is absent the helper falls back to throwing the message with its
natural stack; those framework frames are ignore-listed in the output
but stay available for debugging.
The build-mode error does not yet include a code frame, because
`generateStaticParams` runs in the page-data worker, which lacks the
code-frame support that the prerender export worker has. That is left
for a follow-up. A new end-to-end test covers the redbox in development
for both a literal and a computed empty array, and asserts the per-route
build errors in production using `--debug-build-paths` so each route is
isolated. Transform fixtures cover the literal, computed,
preceding-statement, multiple-return, aliased-export, re-export, and
not-exported cases.
### What?
Bump the default code frame width (used when stdout isn't a terminal)
from 100 to 240 columns.
### Why?
When code frames are captured to a log file or build container, there's
no terminal width to read, so we fall back to a default. 100 columns is
too narrow for log output — build-log viewers soft-wrap or scroll, so
the cost of wrapping too narrow (scattering the caret line away from the
code) is worse than being a bit wide. 240 fits nearly all hand-written
source while still bounding per-frame output so a minified/generated
line can't dump kilobytes into the log.
<!-- NEXT_JS_LLM_PR -->
This takes `chunkingHeuristics` from `next.config.js` and puts it on
`ChunkGroupEntry` so it can be on `ChunkGroupInfo` which will then make
it available in `make_production_chunks`
## Summary
Reworks the cost model of the experimental graph CSS chunking algorithm
(`experimental.cssChunking: 'graph'`, Turbopack only) and retunes its
defaults.
- **Replaces the `moduleFactorCost` option with `weightDistribution`.**
The old `moduleFactorCost` term penalized a chunk purely by `chunk_size
/ group_total_size`, which charged a chunk group even for CSS it fully
needs and never actually measured overshipping.
- **New per-group cost** is `chunk_group_weight * (chunk_size +
request_cost)`, summed over the chunk groups that load a chunk, where
`chunk_group_weight = group_total_size ^ (-weightDistribution)` is
precomputed once per group. `weightDistribution = 0` weights every chunk
group equally; higher values give smaller chunk groups a larger weight,
so the algorithm overships less to small pages at the cost of more
requests. The size weighting subsumes the explicit overship penalty, so
the metric stays chunk-local and the greedy merger is unchanged.
- **Retunes defaults:** `requestCost` `20000` → `100000` (bytes) and
`weightDistribution` default `0.1`.
Config shape (object form): `{ type: 'graph', requestCost?,
weightDistribution? }`. The change is wired end to end through the
config schema/types, `StyleGroupsAlgorithm::Graph`, and the chunking
algorithm.
## Verification
- `cargo test -p turbopack-core --lib style_groups_graph` (54 passed,
incl. a new test asserting `weightDistribution` keeps an unneeded module
out of a small group's chunk)
- `pnpm test-start-turbo test/e2e/app-dir/css-order/css-order.test.ts`
(243 passed, 12 todo) — confirms chunk shapes/request counts at the new
defaults across the `graph` and `graph-object` modes
<!-- NEXT_JS_LLM_PR -->
There are more client-node modules being bundled into `client-browser`
which I'll fix in a followup (with codegen).
Our internal `unstable-rethrow` will now have a version without suffix
(unbundled or client-node) and a client-browser only version
(`.browser`) that bundlers alias to for client-browser which will always
be bundled.
For the specific modules being removed from client-browser, check the
second commit with the actual change. The first commit fixes the
`browser-chunk` test to include `next/src/server` in a snapshot.
Closes#95189
### What
Turbopack's `server-reference-manifest` emitter stores `filename` and
`exportedName` **twice**: once hoisted to the action-entry level
(correct), **and** redundantly inside *every* per-page
`ActionManifestWorkerEntry`. This PR removes the per-worker copies so
the worker map only carries `{ moduleId, async }` — matching the webpack
backend, which already emits them at the entry level only.
The emitter even comments the intent (`// Hoist the filename and
exported_name to the entry level`) and does the hoist — the per-worker
fields are vestigial.
```rust
// crates/next-api/src/server_actions.rs
entry.workers.insert(&key, ActionManifestWorkerEntry {
module_id: loader_id.clone(),
is_async: …,
exported_name, // ← removed (already on entry)
filename: filename.as_ref(), // ← removed (already on entry)
});
entry.exported_name = exported_name; // hoist (kept)
entry.filename = filename.as_ref(); // hoist (kept)
```
### Why it matters
`filename` and `exportedName` are invariant per action, but the worker
map has **one entry per page that can reach the action**, so these
strings get repeated `O(actions × pages)` times. On a large App Router
app (`output: 'standalone'`, `cacheComponents: true`) this is the
dominant cost of the manifest:
- ~1,650 routes, **1,847 references → 589,349 `workers` entries**
- `server-reference-manifest.json` = **170 MB** / `.js` = **182 MB**
(plus the duplicate copy under `.next/standalone`, plus ~314 MB of
per-route manifests)
- A single `'use cache'`/action reachable from a shared client is
registered on **1,638 pages**, each repeating the full `filename` string
Dropping the two redundant per-worker fields removes the largest
repeated strings from every worker entry, shrinking the manifest
substantially with no information loss.
### Why it's safe (consumer-compatible)
- The webpack backend already emits worker entries as `{ moduleId, async
}` only, with `filename`/`exportedName` at the entry level — so this
aligns Turbopack with webpack, not introduces a new shape.
- The only first-party reader,
`packages/next/src/server/mcp/tools/get-server-action-by-id.ts`, reads
`entry.filename` / `entry.exportedName` (entry level) and types
`workers` as `Record<string, any>` — it never reads the per-worker
copies.
### Changes
- `crates/next-core/src/next_manifests/mod.rs` — drop `exported_name` +
`filename` from `ActionManifestWorkerEntry` (keep `module_id`,
`is_async`).
- `crates/next-api/src/server_actions.rs` — stop assigning them in the
worker insert (entry-level hoist unchanged).
### Notes
- Verified by reading the emitter, the manifest types, and the consumer;
I don't have a local Turbopack build, so CI is the source of truth here
— if any manifest **snapshot fixtures** include per-worker
`filename`/`exportedName`, they'll need regenerating.
- Context / measurements: vercel/next.js#95241.
Many fields in TaskStorage store a mix of transient and persistent data.
When we serialize we leverage the `filter_transient` tag (and the
`IsTransient` trait) to avoid persisting it, however, when mutations
occur we are still marking the tasks as `modified` even when we are only
modifying transient data.
This leads to unnecessary persistence cycles.
The solution is simply to not call `track_modification` when the
modification is about transient data items.
For most fields this is handled in the macro accessors, but for
`cell_data` this is managed via some new 'handwritten' mutators
implemented on `TaskGuard`. This is desirable to decrease the number of
times we do registry lookups since most callers who modify the cell_data
already have the ValueType in hand
This is the big change in the `next-` part of the repo to support this.
The code goes through the module graph to find any service workers that
need compiling and then compiles them. It also has the code changes to
`packages/next/src/server/lib/router-utils/filesystem.ts` that serve
these files on their respective routes.
The React Server Components transform validates route segment configs
(rejecting `runtime`, `dynamic`, `revalidate`, and friends under Cache
Components) only for `page`, `layout`, and `route` entries, so metadata
convention files slipped past the check. Because these files compile to
route handlers and accept the same configs, this change extends the
match to `icon`, `apple-icon`, `opengraph-image`, `twitter-image`,
`sitemap`, `robots`, and `manifest`. Exporting `runtime = 'edge'` (or
any other unsupported config) from one of them is now a compile-time
error that matches the behavior for regular routes, instead of surfacing
later as a confusing bundling failure. The `metadata-font` suite, which
intentionally builds edge-runtime image routes, joins the other edge
metadata suites that are excluded from the Cache Components test run.
## Stack Position
(1/7) in the router instrumentation stack.
1. **#94755: router instrumentation: refactor client hook dispatch
(1/7)**
2. #94766: router instrumentation: add transition start context (2/7)
3. #94756: router instrumentation: add transition commit events (3/7)
4. #94765: router instrumentation: add commit route and prefetch context
(4/7)
5. #94757: router instrumentation: add transition abort events (5/7)
6. #94758: router instrumentation: add route mismatch events (6/7)
7. #94737: router instrumentation: add transition settlement events
(7/7)
## What This PR Does
Refactors client instrumentation loading so webpack and Turbopack
provide an ordered array of instrumentation modules to a shared
router-transition dispatcher.
The existing config remains unchanged:
```ts
const nextConfig = {
instrumentationClientInject: ['my-observability-library/client'],
}
```
## Motivation
This refactor lands first, ahead of the new lifecycle events, for two
reasons:
1. **Maintainability — the stack adds more hooks.** Previously the hook
fan-out was generated at build time (a composed
`onRouterTransitionStart` emitted by both the webpack loader and the
Turbopack import map). Each new event (commit, settled, abort, mismatch)
would require regenerating that composition in both bundlers, per hook.
Moving dispatch into one runtime module means new hooks are added once,
in a single place, with no new bundler composition logic per hook.
2. **Fault tolerance.** Dispatch now isolates failures across
instrumentation modules:
- A hook that throws is caught per module, so the remaining modules
still run for that navigation (previously, one throwing hook aborted the
rest).
- Modules whose exports are `null`/`undefined` (side-effect-only) are
filtered out instead of throwing (previously an unguarded existence
check could throw a `TypeError` and break hydration).
## Design
- Injected modules run in array order before the application
instrumentation file.
- With no injected modules, bundlers preserve the direct user
instrumentation alias and the runtime normalizes it to a one-element
array.
- Hooks are read from each module at dispatch time.
- A failing hook does not prevent later modules from running.
- Side-effect-only modules may export `null` or `undefined`.
- The dispatcher can support additional hooks without generating new
bundler composition logic for each hook.
## Not In This PR
- New event arguments or lifecycle hooks
- Transition IDs, timestamps, route data, or prefetch data
## Reviewer Focus
- Webpack and Turbopack produce the same ordered module array.
- Hook fan-out stays in the router-transition dispatcher.
- Ordering, dynamic hook lookup, and per-module error isolation are
preserved.
## Validation
- `pnpm --filter=next types`
- `cargo fmt --all -- --check`
- Instrumentation client E2E coverage in dev and production with webpack
and Turbopack
- Cache Components instrumentation client coverage
<!-- NEXT_JS_LLM_PR -->
Co-authored-by: Jude Gao <jude.gao@vercel.com>