### What?
Correct the fixture marker used to construct the second invalid source
state in the successive App Router HMR test.
### Why?
The test was replacing a marker that does not exist in its MDX fixture.
Because string replacement silently returned the original content,
supposedly successive patches sometimes wrote byte-identical source
while the test harness waited for an HMR completion callback.
A no-op write does not have to produce an HMR update, so the callback
could remain pending until the harness reported a misleading timeout. CI
logs showed Fast Refresh itself completing quickly, which ruled out an
insufficient timeout budget and made extending the timeout the wrong
fix.
### How?
Match the replacement to the fixture's actual marker. This restores four
genuinely distinct source states in every error/recovery cycle,
preserving the intended overlay coverage without changing Turbopack, the
shared HMR helper, or its timeout.
### Verification
- `pnpm test-dev-turbo
test/development/acceptance-app/app-hmr-changes.test.ts`
- Repeated the focused test three times under 14 CPU-contention workers
- Prettier and ESLint on the changed test file
<!-- NEXT_JS_LLM -->
---------
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>
### What
Fixes a shutdown hang in `next build`: a strongly consistent read could
wait forever on a task that shutdown had canceled, which in turn blocked
`stop_and_wait` and hung the build until the test timeout.
Also makes the `hanging_detection` feature usable — it panics due to a
`data` access check today.
### Why the read hangs
1. Shutdown sets `stopped`. A task dispatched after that point is
canceled before it runs, so it has no output.
2. Its parent completes and connects its children.
`process_new_children` uses `!child.has_output()` as the test for "not
computed yet" and marks such a child `InitialDirty` so it gets
scheduled.
3. `make_task_dirty` promotes the canceled task's `SessionDependent` to
plain `Dirty` and clears `current_session_clean`. That is correct for a
live task that must re-run, and wrong for one that never will.
4. `AggregatedDataUpdate::from_task` then snapshots `dirty_state() ==
(true, false)` and contributes a dirty container that is not
session-clean.
5. The parent is itself clean, so its own `update_dirty_state(None ->
None)` early-returns and never evaluates `all_clean_event`. Its
dirty-container count never drains.
6. The strongly consistent reader waits on that event forever, holding a
foreground job that `stop_and_wait` blocks on.
`has_output()` is a proxy for "needs computing", and a canceled task
breaks that assumption.
### The fix
Record a terminal error output when a task execution is canceled.
Make hanging_detection more reliable
## Summary
Previously, when using the built-in route segment override in the
Next.js dev tools (Route Info panel), the UI was not updating: after
toggling a boundary (e.g. `loading.js` or `not-found.js`) you couldn't
switch back unless you closed the modal and reopened it, which slowed
down my workflow. It's not a huge issue, but I thought I'd fix it.
The cause was the segment explorer trie mutating nodes in place, so the
panel's memoized view never picked up insertions until remount. Trie
updates now copy the nodes along the mutated path (copy-on-write) so
changed nodes get fresh identities while untouched subtrees stay shared.
### Before
Switching from one override to another works, but the UI doesn't update
— you have to close and reopen the panel.
https://github.com/user-attachments/assets/39e43ad7-2f14-4b54-bdab-eba51c87a060
### After
Setting an override updates the DevTools UI immediately — no need to
close and reopen the panel.
https://github.com/user-attachments/assets/113887cf-fe07-4d9b-90a5-405b53692a07
## Verification
- `pnpm jest
packages/next/src/next-devtools/dev-overlay/segment-explorer-trie.test.tsx`
- `pnpm test-dev-turbo
test/development/app-dir/segment-explorer/segment-explorer.test.ts`
- Manually verified in a dev app: toggling boundary overrides and "Clear
Segment Overrides" now update the panel live.
<!-- NEXT_JS_LLM -->
Closes#88294
Currently, CSS source maps in Turbopack always use index `0` for all
source file references. This can lead to misleading mappings and makes
debugging CSS generated from multiple sources more difficult.
The fix implemented in #95665 (`params/searchParams` not hanging
correctly in Runtime App Shells during prospective prerenders) was
incomplete. We started using `makeHanging[Search]Params` in the
prospective prerender (if it was an app shell and wouldn't resolve
them), but not during the final prerender. This meant that, when
encoding the cache key, we would only fire `dynamicAccessAbortSignal` in
the prospective prerender:
https://github.com/vercel/next.js/blob/090f1b72753b2d0094235d27d46ffa0da5fc7d35/packages/next/src/server/use-cache/use-cache-wrapper.ts#L2160-L2174
...but the final prerender wasn't using `makeHanging[Search]Params`, so
it would not hit this early exit. The hanging input abort signal would
still correctly abort the key encoding (because it accounts for
`stageController.finalStage`) so the key would still be partial, as
expected. We would continue on, and attempt to look the key up in the
Resume Data Cache. But the prospective prerender exited early and never
put anything under that key, so we would incorrectly end up here, and
log:
https://github.com/vercel/next.js/blob/090f1b72753b2d0094235d27d46ffa0da5fc7d35/packages/next/src/server/use-cache/use-cache-wrapper.ts#L2730-L2734
This is fixed by consistently using `makeHanging[Search]Params` if the
runtime prerender won't reach the stage where they resolve, i.e. if it's
an App Shell. This means we'll trigger `dynamicAccessAbortSignal` while
encoding the cache key in *both* renders, so we'll do the early exit
consistently and avoid the log.
(This feels a bit unsatisfying, because ideally this would all just work
based on stages, which is what I idealistically thought when writing the
code originally. But unfortunately our usage of
`dynamicAccessAbortSignal` for `params/searchParams` requires these
special cases to work consistently. Maybe this can be improved in the
future.)
### Tests
This scenario is covered by the existing `prefetch-runtime` tests -- i
discovered the bug by running them and seeing the `Unexpected cache miss
after cache warming phase` warning. We now assert that the warning is
not logged. The assertion is negative and would also pass if the error
message changed, so i've exported the message as a constant to ensure
`use-cache-wrapper` and the tests stay in sync.
`experimental.collapseAdapterRoutes` now defaults to `true`, so a build
serves several dynamic routes from one entry in the route table that it
passes to an adapter. The default started off in #97774 so that the
behavior could reach projects one at a time. We have tested this
successfully internally, so the default moves back. The option stays in
place, so a build that hits a regression can set it to `false` and get
the table that a build emitted before the collapses.
The four fixtures under `test/production/app-dir/adapter-dynamic-routes`
no longer set the option, because the default now gives them the
collapsed table that their snapshots record. The snapshots themselves do
not change, since the new default produces what the explicit option
produced.
A full deploy test run with the flag on by default was already done as
part of #97738.
## What
- capture stdout and stderr for both direct and shell-fallback
package-manager version probes
- keep successful version output unchanged and return `N/A` when a probe
fails
- add deterministic cross-platform regression coverage with a failing
Yarn shim
Closes#97932
## Why
PR #71134 added an `execSync` fallback so package-manager command shims
can be resolved on Windows. When `stdio` is omitted, Node mirrors stderr
from failed synchronous child processes to the parent, so an unavailable
Yarn or pnpm can print raw shell errors before `next info` reports
`N/A`.
Passing `{ stdio: "pipe" }` to both probes preserves captured stdout and
the Windows fallback while keeping expected probe failures silent. The
change stays scoped to package-manager version detection; shared
registry discovery is unchanged.
## Testing
- `pnpm --filter next typescript`
- `pnpm --filter next build`
- `pnpm test-types --pretty false`
- ESLint and Prettier on both changed files
- `pnpm test-start test/e2e/cli/cli.test.ts` (66 passed, 1 skipped)
- built-CLI reproduction with npm available and Yarn/pnpm absent: both
report `N/A` with no Yarn/pnpm command-not-found output
- autoreview with GPT-5.6 Sol xhigh and Claude Opus 5 xhigh: zero
findings
---------
Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>
## Summary
Remove two inert entries from the deploy-test manifest:
- the entry for
`test/e2e/app-dir/options-request/options-request.test.ts`, which no
longer exists
- the `test/production/standalone-mode/**/*` glob, which cannot match
the manifest's e2e-only include rule
This leaves the hard exclusion list limited to live, selectable test
suites so follow-up deploy-testing work starts from an accurate
inventory.
## Verification
- Parsed `test/deploy-tests-manifest.json` and confirmed all 26
remaining exclusions resolve to files selected by the manifest's include
rule.
<!-- NEXT_JS_LLM -->
## Summary
- enable the existing auto-close workflow once per hour
- keep manual dispatch available
- leave the workflow logic and permissions unchanged
## Verification
- production `workflow_dispatch` succeeded end to end:
https://github.com/vercel/next.js/actions/runs/33788887152
- production returned a clean empty-queue response after OIDC
authentication and Vercel Trusted Sources validation
- Prettier and `git diff --check` pass
## Summary
- return `400 Bad Request` when a Server Action reference ID does not
match the expected format
- return `409 Conflict` when a well-formed action ID is unavailable in
the current deployment, including deployment skew
- apply the same classification to IDs from the `Next-Action` header and
multipart MPA forms, including direct and bound actions in Node and Edge
runtimes
- return generic text bodies matching the client error category, without
exposing action-ID details
- derive marked forwarded-action responses from the original action ID
so an unexpected or mixed-version worker response cannot change the
client error semantics
Fetch actions previously had dedicated 404 handling, while equivalent
MPA form submissions fell through to the generic error path and returned
500. This removes that transport-dependent behavior and distinguishes
malformed or scanning traffic from legitimate stale clients without
treating either case as a missing page or an internal server failure.
## Verification
- `pnpm test-dev-turbo
test/e2e/app-dir/actions-unrecognized/actions-unrecognized.test.ts`
- `pnpm test-dev-turbo test/e2e/app-dir/actions/app-action.test.ts -t
'forward.*action'`
- `pnpm test-start-turbo
test/e2e/app-dir/actions-unrecognized/actions-unrecognized.test.ts`
- `pnpm test-dev-webpack
test/e2e/app-dir/actions-unrecognized/actions-unrecognized.test.ts`
- `pnpm test-dev-turbo
test/e2e/app-dir/no-server-actions/no-server-actions.test.ts`
<!-- NEXT_JS_LLM -->
## Summary
- add a manually dispatchable workflow; the hourly schedule is left
commented until initial production verification
- keep the workflow YAML small and place delivery in
`.github/scripts/next-maintainer-auto-close.js` for normal code review
- use a short-lived exact-audience GitHub Actions OIDC token with no
long-lived secret
- trust the authenticated queue contract instead of reimplementing its
Zod validation in the workflow
- post the verifier-authored comment and close with GitHub native
completed, not planned, or duplicate state reasons
- retain one invisible marker only to prevent duplicate public comments
across retries
If an issue is open with the marker, the workflow leaves it open. This
intentionally lets a human reopen win and avoids timeline
reconstruction.
## Permissions
The job grants only `contents: read`, `id-token: write`, and `issues:
write`; every other permission remains none. Both GitHub actions are
pinned to full commit SHAs. Checkout is sparse to the one trusted
JavaScript file and has credential persistence disabled.
## Verification
- mocked delivery harness passes ten scenarios: empty queue, completed,
not planned, duplicate, pull-request rejection, already-closed recovery,
independent close, open marker, transferred issue, and transient failure
- `node --check`, Prettier, and ESLint pass for the extracted
implementation
- Vercel Agent Review, Vercel Security Review, Socket Security, workflow
change detection, and documentation validation pass
- after merge, dispatch the registered workflow on `canary` and add the
production run link here before enabling the hourly schedule
## Dependency
This is the narrow GitHub write-side companion to
vercel-labs/next-maintainer-agent#541, which is deployed.
vercel-labs/next-maintainer-agent#546 further reduces the queue DTO and
changes the delivery limit to 25 first claims per rolling week; this
workflow is compatible with both DTO versions.
So I was playing around with profiling some very simple next apps when I
noticed this. Basically, because of how things were set up we were
spending a good chunk of our build time analyzing files we were going to
reject. I went around and around trying different combinations to make
sure we ignore those files (and only the files we should ignore) to
speed up build times. Since it is a constant impact, mostly on small
projects, but on some, it can make a big difference. I had some
adversarial rounds with agents to see if they can break the config in
the ways the old comments hints at why we didn't do that. And this final
round finally couldn't get it to break.
For a v0-generated Hello World app, compile time is 28.0% faster
(+/-1.7%) and a full clean build is 15.9% faster (+/- 1.2%).
---------
Co-authored-by: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com>
Enabling `experimental.taint` makes Next.js taint `process.env` for you
(`app-render.tsx`), so passing it whole to a Client Component throws.
The docs never mentioned this.
Adds it to the existing "Good to know" callout, plus a caveat that the
taint is by object reference, so `process.env.MY_VAR` and `{
...process.env }` still pass through.
## Summary
Adds an agent eval covering migration of the legacy on-demand ISR
pattern: `force-static`, `revalidate`, and an empty
`generateStaticParams` result.
The assertions require the agent to enable Cache Components, preserve
the `generateStaticParams` export, and replace its empty result with at
least one parameter instead of deleting the function. The fixture also
runs the Cache Components adoption skill with the established 30-minute
timeout.
Updates the Cache Components migration guide to link each legacy route
segment config to its migration instructions. The `generateStaticParams`
section now explains that removing the export opts a dynamic route out
of ISR and links to the ISR with Cache Components guide.
## Eval results
### Before the guide change
| Treatment | Result | Duration | Behavior |
| --- | --- | ---: | --- |
| Baseline | Passed (1/1) | 482.9s | Preserved `generateStaticParams`
and returned a real seed. |
| Bundled docs / AGENTS.md | Failed (0/1) | 444.7s | Deleted
`generateStaticParams`, incorrectly treating omission as equivalent to
returning an empty array. |
| Cache Components skill | Passed (1/1) | 527.6s | Preserved
`generateStaticParams` and returned a real seed. |
The failed treatment was classified as a model failure. It read the
bundled migration guide but concluded that deleting
`generateStaticParams` preserved the defer-all-paths-to-runtime
behavior. The eval catches that loss of on-demand ISR semantics.
The full run used the identical fixture immediately before its numeric
identifier was changed from `agent-044` to the final `agent-054` name.
The fixture and assertions were unchanged.
### After the guide change
| Treatment | Result | Duration | Behavior |
| --- | --- | ---: | --- |
| Baseline | Passed (1/1) | 739.5s | Preserved `generateStaticParams`,
returned `FEATURED_EVENT_SLUG`, and compared the migrated route with the
original behavior. |
| Bundled docs / AGENTS.md | Passed (1/1) | 393.2s | Read the revised
migration and ISR guides, preserved `generateStaticParams`, and returned
`FEATURED_EVENT_SLUG`. |
| Cache Components skill | Passed (1/1) | 547.3s | Preserved
`generateStaticParams`, returned `FEATURED_EVENT_SLUG`, and verified the
build and representative routes in development. |
In the first controlled pair, the bundled-docs treatment changed from
failing to passing. Its final migration kept the route eligible for ISR,
preserved the 60-second revalidation behavior with `use cache` and
`cacheLife`, and produced a passing build.
### Additional variance check
The bundled-docs treatment was run once more against each guide version:
| Guide version | First sample | Second sample | Observed result |
| --- | --- | --- | --- |
| Before this change | Failed (444.7s) | Passed (455.6s) | 1/2 passed |
| After this change | Passed (393.2s) | Passed (425.2s) | 2/2 passed |
The second pre-change sample shows that the previous wording could still
lead the agent to the correct migration. The revised wording passed both
observed samples, suggesting that the explicit ISR consequence improves
reliability. Two samples per version are not enough to claim a stable
pass rate.
## Verification
- `pnpm --filter=next build`
- `pnpm eval agent-054-cache-components-empty-static-params --dry`
- `pnpm eval agent-054-cache-components-empty-static-params`
<!-- NEXT_JS_LLM -->
## Summary
Upgrades the vendored `web-vitals` dependency to v6.0.1 and enables
soft-navigation reporting for CLS, INP, and LCP. This lets
`useReportWebVitals` report metrics against the navigation where they
occurred, including `navigationURL` for route attribution.
The upgrade also removes the obsolete FID integration and compatibility
patch, regenerates the standard and attribution bundles, and updates the
public metric types and documentation to match the current upstream API.
Related to #97161.
## Verification
- Added production-mode coverage for initial and soft-navigation Web
Vitals reporting with both Turbopack and Webpack.
<!-- NEXT_JS_LLM -->
## What?
Adds per-family compression configuration to `turbo-persistence` and
configures the Turbopack filesystem cache according to each keyspace's
access pattern:
| Family | Compression | Reason |
| --- | --- | --- |
| Infra | LZ4 HC level 4 | Preserve LZ4 decode performance while
improving write-time compression |
| TaskMeta | LZ4 HC level 4 | Preserve latency-sensitive metadata reads
|
| TaskCache | LZ4 HC level 4 | Preserve latency-sensitive task-cache
reads |
| TaskData | zstd level 3 | Prioritize the dominant disk-size
opportunity |
The low-level configuration exposes only the three presets this database
needs: LZ4, LZ4 HC4, and zstd3. Compression is applied consistently to
SST blocks, blob values, and compaction output. Each database owns a
`ThreadLocal` that lazily creates one lock-free zstd decompressor per
participating thread; the linked zstd reports each context as about 96
KiB, and all contexts are released when the database drops.
Each meta file stores its family's length-prefixed, bincode-encoded
compression preset once. SST and blob headers remain unchanged,
including the 12.5% minimum-savings fallback to uncompressed blocks. On
normal database open, the stored marker must match the runtime
`FamilyConfig`; a mismatch, invalid/truncated preset, or pre-marker meta
file is rejected before reads are served. This intentionally requires a
fresh/versioned cache after a codec change, consistent with
`turbo-persistence`'s existing no-cross-version-compatibility policy.
The marker also removes the duplicated keyspace-to-codec mapping from
`sst_inspect`: it reads the codec directly from metadata.
`turbo-persistence-tools` can inspect metadata without supplying a
Turbopack-specific family configuration.
## Why?
LZ4 decode performance is important for cache query latency, but using
one codec for every keyspace leaves a substantial disk-size opportunity
in TaskData. The families do not have the same tradeoff: metadata and
task-cache reads should stay on the LZ4 decoder, while TaskData can
spend modestly more CPU to reduce persistent cache size.
The final three-workload A/B reduced fresh cache directories by **20.61%
overall**, with no measured cold/write or warm/read regression (the
observed -3.42% / -2.39% should be treated as noise, not a speed claim).
## How?
### Real Next.js cache A/B
The complete A/B was re-run after the final fixed-preset,
bincode-marker, and database-owned thread-local revision. Both the
unchanged base worktree and final proposed working tree were
bootstrapped with `pnpm install --frozen-lockfile` and `pnpm build-all`,
using locally compiled native bindings on the same 8-vCPU Intel Xeon VM
(16.3 GiB RAM, Linux 6.18.40, Node 24.14.1). Each workload used three
fresh-cache builds and five warm-cache rebuilds; the table reports
medians.
```sh
TURBO_ENGINE_IGNORE_DIRTY=1 NEXT_TELEMETRY_DISABLED=1 \
node packages/next/dist/bin/next build <fixture> --turbopack
```
`test/e2e/filesystem-cache` additionally used `ENABLE_CACHING=1`. The
other fixtures temporarily enabled
`experimental.turbopackFileSystemCacheForBuild`; those fixture changes
are not in this PR.
| Workload | Cache size | Cold/write | Warm/read |
| --- | ---: | ---:| ---: |
| `test/e2e/filesystem-cache` | **-20.62%** | -2.24% | -3.00% |
| `test/e2e/app-dir/app-rendering` | **-20.11%** | -6.04% | -1.47% |
| `test/e2e/app-dir/client-reference-chunking` | **-21.02%** | -1.45% |
-3.56% |
| Combined medians | **-20.61%** (96.810 → 76.862 MiB) | **-3.42%** |
**-2.39%** |
Per-family inspection of a final filesystem-cache build, using the
marker-derived codec:
| Family | Total file size | Value-block savings |
| --- | ---: | ---: |
| Infra | 52 B | n/a |
| TaskMeta | 6.31 MB | 29% |
| TaskData | 18.17 MB | 66% |
| TaskCache | 1.65 MB | n/a |
The originally planned `app` and `app-static` fixtures could not run as
standalone builds: `app` hit the existing Edge-runtime `process.cwd()`
build failure, and `app-static` required an external data endpoint
during page-data collection. They were replaced with the two successful
repository fixtures shown above under the same base/proposed
methodology.
### Temporary crate benchmark instrumentation
The codec-specific Criterion cases were used locally to gather receipts,
then removed from the delivered patch per review.
| Codec | Synthetic write | vs LZ4 | Uncached get | Cached get | DB size
|
| --- | ---: | ---: | ---: | ---: | ---: |
| LZ4 | 38.173 ms | baseline | 7.3004 µs | 10.735 µs | 21.59 MiB |
| LZ4 HC4 | 256.31 ms | **+571.7%** | 3.4853 µs | 8.5467 µs | 21.57 MiB
|
| zstd3 | 57.790 ms | **+51.4%** | 6.2460 µs | 9.5323 µs | 21.49 MiB |
The synthetic workload applies one codec to the entire database and
barely separates codecs by size, so it is not representative of the real
TaskData distribution. The all-HC write regression is real but confined
to smaller families in the actual configuration. Cached reads do not
decompress blocks, and the read samples contain substantial
ordering/outlier noise. The real-cache A/B is the read-regression guard.
## Vercel Site
### Size
2.9G canary
2.2G this PR
a 25% savings
### Performance
Cold build (n=5), warm build (n=3), medians:
| Scenario | Metric | Canary | Compression | Δ |
|---|---|---|---|---|
| **Cold** | wall (s) | 71.28 | 72.86 | +2.2% |
| | user (s) | 555.21 | 570.23 | +2.7% |
| | sys (s) | 94.49 | 95.00 | +0.5% |
| | maxRSS (GB) | 17.95 | 17.98 | +0.2% |
| **Warm** | wall (s) | 15.00 | 15.32 | +2.1% |
| | user (s) | 7.59 | 9.98 | +31% |
| | sys (s) | 12.79 | 12.87 | +0.6% |
| | maxRSS (GB) | 3.78 | 3.86 | +2.0% |
maxRSS converted at 1 GB = 2^30 bytes.
Looking at tracing data i see in a cold build `persist` span went from
5.06s duration using 63s cpu time to 6.04s using 73s cpu time, a ~20%
regression. Which explains the cpu regression and the time progression.
Because the time regression is in the `persist` span during shutdown the
added latency is somewhat hidden.
Of course we also see a small wall/user time regression in warm builds
due to the extra decompression costs for zstd.
### Verification
- `cargo test -p turbo-persistence` (79 tests)
- `cargo test -p turbo-tasks-backend` (99 unit tests plus integration
suites)
- `cargo test -p turbo-persistence-tools`
- `cargo test -p turbo-persistence --all-targets --no-run`
- `cargo fmt --all -- --check`
- `cargo clippy -p turbo-persistence -p turbo-persistence-tools -p
turbo-tasks-backend --all-targets -- -D warnings`
- `cargo check -p turbo-persistence -p turbo-persistence-tools -p
turbo-tasks-backend`
- `pnpm lint-ast-grep`
- `pnpm build-all`
<!-- NEXT_JS_LLM -->
<!-- fleet 621238c3-5a53-431b-8661-8c5b2920d55d -->
---------
Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
Co-authored-by: Luke Sandberg <210140+lukesandberg@users.noreply.github.com>
A `describe` gated by a lazy `// @force-gate` (a condition read off the
fixture's resolved config, like `cacheComponents`) cannot be skipped at
collection time, so the gate runtime force-passes its tests at runtime
and `nextTestSetup` skips the fixture build. Hooks registered inside
such a `describe` still ran, because only the `it`/`test` globals were
wrapped. A hook that touches the `next` instance (for example a
`beforeEach` that reads `next.cliOutput`) then failed with `next
instance is not initialized yet` and failed the supposedly skipped
suite.
The gate runtime now wraps the
`beforeAll`/`afterAll`/`beforeEach`/`afterEach` globals as well and
skips a hook registered under a lazy `@force-gate` when that gate
evaluates false against the fixture's resolved config. Only a false lazy
force-gate skips a hook: an inverted `@gate` still runs its tests and
therefore needs its hooks, and a static `@force-gate` already skips the
whole `describe` at collection time. The hooks `nextTestSetup` registers
itself are marked with the new `ungatedHook` helper so that the skip
decision and the fixture cleanup still run.
The `cacheComponents disabled, edge app router` describe in
`test/e2e/cache-handlers-upstream-wiring` is converted from the
`process.env.__NEXT_CACHE_COMPONENTS` `describe.skip` ternary to `//
@force-gate !cacheComponents`, replacing a fake-green skip and
exercising the fixed path. The `!deploy` conversion of the remaining
describes is left to the branch that introduces the `deploy` condition.
Co-authored-by: Claude Code (kimi-k3[1m]) <noreply@anthropic.com>
There have been breaking changes to the AST:
- `BlockStmtOrExpr` is now `ArrowFunctionBody`
- function bodies are `FunctionBody` instead of `BlockStmt`
- JSX text literals are `Wtf8Atom`, instead of `Atom`
Adds garbage collection, built on the `parent_count` reference counting
### How collection works
* we scan all storage shards for collectible tasks to seed the sweep
* For each collectible task we
1. mark it as deleted
2. remove all outgoing edges
3. queue new tasks for deletion if the edge removals triggered it
For removing edges we rely on the existing `CleanupOldEdges` operation,
though it is enhanced to collect the tasks that become collectible as
edges are removed
Deletion markers are transformed into tombstones by persistence and then
dropped from memory by eviction.
### Coordinator changes
GC mutates the graph, so we exclude other tasks from running while it
works. When gc completes we hand off to persistence which switches to
the existing copy on write mode.
The subtle piece is `GcPhase::into_snapshot`: GC has to hand its
exclusion directly to the snapshot that tombstones what it collected.
Swapping the flags under one lock leaves no window where an operation
could start and resurrect a just-collected task.
### Enabling it
Off by default; `TURBO_ENGINE_GC` turns it on, also requires eviction to
be enabled (otherwise deleted tasks persist in RAM)
`prevent_gc()` becomes real, pinning the calling task through
`transient_ref_count`.
Avoid spawning a worker thread to run turbopack
The typical arguments for worker threads are that they isolate the JS
heap and enable parallelism, but turbopack does both these things by
default (native heap is isolated, the js deps are minor and have a lot
of overlap with existing next server deps).
There is also no substantial parallelism opportunity from worker threads
that aren't immediately delivered by turbopacks execution model
## Benchmarks
Measured with a paired A/B harness that alternates `head` and `base`
builds so thermal drift and background load cancel out, discarding a
warmup build per variant and clearing `.next` before each run. Both
variants are the same commit except for this change. 35 paired
iterations on a default `create-next-app` (App Router, TS), Node
20.11.1, Apple silicon.
| Metric | this PR | canary | Diff | 95% CI | Wins |
| --- | --- | --- | --- | --- | --- |
| `next-build` total | 2478 ms | 2595 ms | **-117 ms (4.5%)** | [50,
183] | 30/35 |
| `run-turbopack` | 1228 ms | 1335 ms | -107 ms (8.0%) | [62, 152] |
31/35 |
| overhead (total - turbopack) | 1250 ms | 1260 ms | -10 ms (0.8%) |
[-27, 46] | 20/35 |
| wall clock | 2775 ms | 2888 ms | -112 ms (3.9%) | [45, 180] | 30/35 |
| peak RSS | 1175 MiB | 1195 MiB | **-19 MiB (1.6%)** | [3.4, 35.3] |
22/35 |
Significant by both a paired t-test and a Wilcoxon signed-rank test (p =
0.0001 for total build time, p = 0.025 for RSS). The Wilcoxon is
included because a couple of slow builds per run would otherwise
dominate the variance.
Nearly all of the saving lands inside the `run-turbopack` span rather
than around it, which is expected: that span wraps the worker spawn, the
IPC to marshal the build across, and the teardown, so deleting the
worker removes work from inside it.
The win is proportionally smaller on larger apps, where compilation
dominates. On `bench/basic-app` (~400 source modules, ~19s builds) the
same ~115 ms is well inside the noise -- 12 paired iterations there
showed no significant difference on any metric. So this is a real but
small improvement that is only user-visible on small builds:
| Metric | this PR | canary | Diff | 95% CI |
| --- | --- | --- | --- | --- |
| `next-build` total | 19313 ms | 19436 ms | -123 ms (0.6%) | [-256,
502] |
| overhead (total - turbopack) | 1424 ms | 1415 ms | +9 ms (0.6% slower)
| [-52, 70] |
Caveats: single machine and two fixture shapes. Peak RSS is the build
process's own footprint (`/usr/bin/time -l`), which includes the worker
thread's isolate but not the separately spawned static-generation
workers. A CI runner with fewer cores would plausibly show a larger
effect.
### What?
Follow-up to #97761, which was auto-submitted before @bgw's review
comments were addressed. Comment-only changes; no behavior change.
- Name the nulled-out out-params in the `mi_process_info` call, applying
the suggestion as written.
- Trim the `memory_usage` doc comment to a brief description plus a link
to the mimalloc docs. `mi_process_info` is absent from mimalloc's
published doxygen docs, so the link points at the `libmimalloc-sys`
rustdoc, which reproduces the header's table describing each figure. It
is a plain URL rather than an intra-doc link because `libmimalloc-sys`
is an optional dependency — an intra-doc link fails to resolve under
`--no-default-features`.
- Trim the test's doc comment to the single suggested line.
The `#[global_allocator]` suggestion matched the existing code exactly,
so nothing to change there. Left the `vec!` in the test alone, per "this
is fine because every test in this crate should expect this global
allocator".
While in the same doc comment, dropped the `[`self::counter`]` link,
which was raising a `private_intra_doc_links` warning.
### How?
Verified with `cargo fmt --check`, `cargo clippy -p turbo-tasks-malloc
--all-targets`, `cargo test -p turbo-tasks-malloc` (6 passed), and
`cargo doc` under `RUSTDOCFLAGS="-D warnings"` with both default and
`--no-default-features`.
### What?
`turbo-tasks-malloc` keeps a process-wide `ALLOCATED` atomic to answer
`memory_usage()`. To optimize performance thread local buffers were
maintained but this lead to code size issues. So instead we rely on
mimalloc apis saving 1.6M of binary size
### Why not ask the OS?
That was the obvious alternative, and it does not work well. The natural
per-platform figures are not the same quantity: macOS `phys_footprint`
and Windows `PrivateUsage` account for compressed and swapped pages, but
Linux `VmRSS` does not — so under swap or zram the Linux number can read
flat, or fall, while real consumption climbs. Closing that gap means
`/proc/self/smaps_rollup` (`Pss + SwapPss`), which is not the cheap
one-line read it looks like, it takes on the order of [100ms per
4GB](https://github.com/ncabatoff/process-exporter/issues/246), and it
would contend with other allocations in our process.
### How?
`memory_usage()` reads `current_commit` from `mi_process_info`. We fall
back to the `ALLOCATED` atomic only when `custom_allocator` is off.
### Behaviour changes
**It does not track frees in lock step.** mimalloc reuses and purges
pages on its own schedule or when eviction calls `collect`
### Size
Rust inlines `#[global_allocator]` methods into every allocation site,
so the buffer arithmetic and the branch guarding it were duplicated
across the whole binary. Dropping them from the default build is where
the win comes from.
`libnext_napi_bindings.dylib`, release profile as shipped (thin LTO,
`codegen-units = 1`).
| Section | canary | this PR | Δ |
|---|---:|---:|---:|
| `__text` | 64,833,760 | 63,319,776 | **−1,513,984** |
| `__eh_frame` | 8,966,904 | 8,748,464 | −218,440 |
| `__gcc_except_tab` | 2,234,428 | 2,229,484 | −4,944 |
| `__unwind_info` | 1,489,072 | 1,484,644 | −4,428 |
| **Total** | | | **−1,741,796 = −1.66 MiB** |
### Performance
`cargo bench -p turbo-tasks-malloc --bench allocation`, macOS aarch64,
revisions interleaved across 3 rounds so drift hits both arms equally.
Figures are the paired per-round deltas.
| benchmark | canary | this PR | mean Δ | per-round |
|---|---:|---:|---:|---|
| `alloc_dealloc` | 6.65 ns | 6.45 ns | **−2.9%** | −3.7% .. −2.4% |
| `alloc_realloc_dealloc` | 18.62 ns | 16.76 ns | **−10.0%** | −10.2% ..
−9.7% |
Removing the counter takes the buffer arithmetic and its guarding branch
out of every allocation, and a realloc pays that twice — which is
roughly the shape of the result. That said, the absolute numbers are
small (a couple of hundred picoseconds on `alloc_dealloc`) and this
benchmark is noisy, so treat the direction as the signal rather than the
magnitude.
Cache components are only compatible with Node.js runtime anyway. So
this was basically dead code. Removing it doesn't break any existing
functionality.
The last remaining `Lock Threads` failures are likely due to missing
permissions for discussions which `dessant/lock-threads` handles by
default.
This disables discussion handling to get the workflow green to start
tracking regressions. We can discuss if we want to lock inactive
discussions in a follow-up (which would be new behavior this workflow
never performed).
Co-authored-by: Claude Code (kimi-k3[1m]) <noreply@anthropic.com>
## Summary
- add the preload pattern to the current App Router data fetching guide
- distinguish automatic fetch memoization, React cache for non-fetch
data, and Cache Functions
- clarify request-scoped private caching and when stale: Infinity should
not constrain the route stale time
The previous-model caching guide remains unchanged.
## Verification
- Prettier on both edited docs
- alex on both edited docs
- git diff --check
- Not run: full framework build because this is a documentation-only
change
<!-- NEXT_JS_LLM -->
- PR #97931 temporarily exempted sharp@0.35.4 and its @img/* binaries to
ship same day.
- Its now past the 48-hour minimum release age.
- This PR removes those temporary exemptions.
The view-transitions eval kept requiring a config option that #96098
removed, so models following the current docs were failed for correct
solutions. Evals only, no runtime change.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
### What?
Centralizes related <code>"use cache"</code> scope errors and rewrites
them as shorter, actionable messages with consistent <code>Learn
more:</code> links. It also updates the reachable revalidation errors
for rendering and <code>generateStaticParams</code>.
### Why?
The previous messages were long, inconsistent, and sometimes missing
documentation links. Some new cache-specific revalidation messages were
also hidden by an earlier render-phase error. The new messages name the
constraint, give the immediate fix, and link to the relevant
documentation.
### How?
Adds centralized error factories and dedicated error pages for request
data, cache configuration, private-cache composition, and revalidation.
When an active App Router route is available, the messages include it.
The error pages use consistent terminology and complete examples. The
<code>unstable_cache()</code> reference now documents the
request-dependent operations that these errors reject.
### Before and after
Route-based examples use <code>/products</code>. Bracketed values are
alternatives in matching order. For example, <code>[E1482, E1486,
E1489]</code> maps to <code>[headers(), cookies(), request.url]</code>.
Each runtime error contains one alternative, but the table groups
messages with the same template.
#### Messages that include the route
| Cases | Before | After |
| --- | --- | --- |
| E1480 · <code>searchParams</code> in <code>"use cache"</code> | Route
/products used <code>searchParams</code> inside "use cache". Accessing
dynamic request data inside a cache scope is not supported. If you need
some search params inside a cached function await
<code>searchParams</code> outside of the cached function and pass only
the required search params as arguments to the cached function. See more
info here: https://nextjs.org/docs/messages/next-request-in-use-cache |
Route "/products": <code>searchParams</code> can't be read inside
<code>"use cache"</code>. Await it outside the cached function and pass
what you need as an argument.<br><br>Learn more:
https://nextjs.org/docs/messages/next-request-in-use-cache |
| [E1482, E1486, E1489] · [<code>headers()</code>,
<code>cookies()</code>, <code>request.url</code>] in <code>"use
cache"</code> | Route /products used [<code>headers()</code>,
<code>cookies()</code>, <code>request.url</code>] inside "use cache".
Accessing Dynamic data sources inside a cache scope is not supported. If
you need this data inside a cached function use [<code>headers()</code>,
<code>cookies()</code>, <code>request.url</code>] outside of the cached
function and pass the required dynamic data in as an argument. See more
info here: https://nextjs.org/docs/messages/next-request-in-use-cache |
Route "/products": [<code>headers()</code>, <code>cookies()</code>,
<code>request.url</code>] can't be read inside <code>"use cache"</code>.
Read it outside the cached function and pass what you need as an
argument.<br><br>Learn more:
https://nextjs.org/docs/messages/next-request-in-use-cache |
| [E1481, E1485, E1492] · [<code>cookies()</code>,
<code>request.url</code>, <code>headers()</code>] in
<code>unstable_cache()</code> | Route /products used
[<code>cookies()</code>, <code>request.url</code>,
<code>headers()</code>] inside a function cached with
<code>unstable_cache()</code>. Accessing Dynamic data sources inside a
cache scope is not supported. If you need this data inside a cached
function use [<code>cookies()</code>, <code>request.url</code>,
<code>headers()</code>] outside of the cached function and pass the
required dynamic data in as an argument. See more info here:
https://nextjs.org/docs/app/api-reference/functions/unstable_cache |
Route "/products": [<code>cookies()</code>, <code>request.url</code>,
<code>headers()</code>] can't be read inside
<code>unstable_cache()</code>. Read it outside the cached function and
pass what you need as an argument.<br><br>Learn more:
https://nextjs.org/docs/app/api-reference/functions/unstable_cache |
| [E1484, E1491] · <code>draftMode().enable()</code> in
[<code>unstable_cache()</code>, <code>"use cache"</code>] | Route
/products used "draftMode().enable()" inside [a function cached with
<code>unstable_cache()</code>, "use cache"]. The enabled status of
<code>draftMode()</code> can be read in caches but you must not enable
or disable <code>draftMode()</code> inside a cache. See more info here:
[https://nextjs.org/docs/app/api-reference/functions/unstable_cache,
https://nextjs.org/docs/messages/next-request-in-use-cache] | Route
"/products": <code>draftMode().enable()</code> can't be called inside
[<code>unstable_cache()</code>, <code>"use cache"</code>]. Draft mode
can be read inside a cached function, but enabling or disabling it must
happen outside.<br><br>Learn more:
[https://nextjs.org/docs/app/api-reference/functions/unstable_cache,
https://nextjs.org/docs/messages/next-request-in-use-cache] |
| [E1488, E1499] · <code>connection()</code> in
[<code>unstable_cache()</code>, <code>"use cache"</code>] | Route
/products used <code>connection()</code> inside [a function cached with
<code>unstable_cache()</code>, "use cache"]. The
<code>connection()</code> function is used to indicate the subsequent
code must only run when there is an actual request, but caches must be
able to be produced before a request, so this function is not allowed in
this scope. See more info here:
[https://nextjs.org/docs/app/api-reference/functions/unstable_cache,
https://nextjs.org/docs/messages/next-request-in-use-cache] | Route
"/products": <code>connection()</code> can't be called inside
[<code>unstable_cache()</code>, <code>"use cache"</code>] because cached
functions may run during prerendering, without an incoming request. Call
it outside the cached function.<br><br>Learn more:
[https://nextjs.org/docs/app/api-reference/functions/unstable_cache,
https://nextjs.org/docs/messages/next-request-in-use-cache] |
| E1494 · <code>connection()</code> in <code>"use cache: private"</code>
| Route /products used <code>connection()</code> inside "use cache:
private". The <code>connection()</code> function is used to indicate the
subsequent code must only run when there is an actual navigation
request, but caches must be able to be produced before a navigation
request, so this function is not allowed in this scope. See more info
here: https://nextjs.org/docs/messages/next-request-in-use-cache | Route
"/products": <code>connection()</code> can't be called inside <code>"use
cache: private"</code> because private cached functions may run during
prefetching, without a navigation request. Call it outside the cached
function.<br><br>Learn more:
https://nextjs.org/docs/app/api-reference/directives/use-cache-private |
| [E1483, E1495] · <code>revalidateTag("products")</code> in [<code>"use
cache"</code>, <code>unstable_cache()</code>] | Route /products used
"revalidateTag products" inside [a "use cache", a function cached with
"unstable_cache(...)"] which is unsupported. To ensure revalidation is
performed consistently it must always happen outside of renders and
cached functions. See more info here:
https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering
| Route "/products": <code>revalidateTag("products")</code> can't be
called during render, inside a cached function, or inside
<code>generateStaticParams</code>. Call it from a Server Action or Route
Handler instead.<br><br>Learn more:
https://nextjs.org/docs/messages/revalidate-in-use-cache |
| During render | Route /products used "revalidateTag products" during
render which is unsupported. To ensure revalidation is performed
consistently it must always happen outside of renders and cached
functions. See more info here:
https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering
| Route "/products": <code>revalidateTag("products")</code> can't be
called during render, inside a cached function, or inside
<code>generateStaticParams</code>. Call it from a Server Action or Route
Handler instead.<br><br>Learn more:
https://nextjs.org/docs/messages/revalidate-in-use-cache |
| Inside <code>generateStaticParams</code> | Route /products used
"revalidateTag products" inside <code>generateStaticParams</code> which
is unsupported. To ensure revalidation is performed consistently it must
always happen outside of renders and cached functions. See more info
here:
https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering
| Route "/products": <code>revalidateTag("products")</code> can't be
called inside <code>generateStaticParams</code>. Call it from a Server
Action or Route Handler instead.<br><br>Learn more:
https://nextjs.org/docs/messages/revalidate-in-use-cache |
#### Other messages
| Cases | Before | After |
| --- | --- | --- |
| [E1477, E1490] · Nested cache with [short <code>expire</code>,
<code>revalidate: 0</code>] | A "use cache" with [short
<code>expire</code> (under 5 minutes), zero <code>revalidate</code>] is
nested inside another "use cache" that has no explicit
<code>cacheLife</code>, which is not allowed during prerendering. Add
<code>cacheLife()</code> to the outer "use cache" to choose whether it
should be prerendered [with longer <code>expire</code>, with non-zero
<code>revalidate</code>] or remain dynamic [with short
<code>expire</code>, with zero <code>revalidate</code>]. Read more:
https://nextjs.org/docs/messages/nested-use-cache-no-explicit-cachelife
| Route <code>"/products"</code>: A nested <code>"use cache"</code> with
[a short <code>expire</code> (under 5 minutes), <code>revalidate:
0</code>] is inside an outer <code>"use cache"</code> that has no
<code>cacheLife()</code>. Add <code>cacheLife()</code> to the outer one
to choose whether to prerender it [with a longer <code>expire</code>,
with a non-zero <code>revalidate</code>] or keep it dynamic [with a
short <code>expire</code>, with <code>revalidate:
0</code>].<br><br>Learn more:
https://nextjs.org/docs/messages/nested-use-cache-no-explicit-cachelife
|
| E1478 · External promise | Filling a "use cache" entry appears to be
stuck on shared state from the outer render scope. The same function
completed when run in isolation, which usually means a module-scoped
value (for example a top-level Map used to dedupe fetches) is joining a
promise created outside the cache. "use cache" already dedupes calls
with the same arguments within a request and across requests on the same
server instance, so the surrounding dedupe layer is both unnecessary and
the likely cause. Remove it and rely on "use cache" alone for
deduping.<br><br><em>No documentation link.</em> | Route
<code>"/products"</code>: A <code>"use cache"</code> function is
awaiting a promise created outside it. The same call completed when run
in isolation, so a module-scoped value (often a top-level
<code>Map</code> used to dedupe fetches) is most likely blocking it.
<code>"use cache"</code> already dedupes calls with the same arguments.
Remove the surrounding dedupe layer.<br><br>Learn more:
https://nextjs.org/docs/messages/next-request-in-use-cache |
| [E1479, E1498] · [<code>cacheTag()</code>, <code>cacheLife()</code>]
outside a cached function | [<code>cacheTag()</code>,
<code>cacheLife()</code>] can only be called inside a "use cache"
function.<br><br><em>No documentation link.</em> | Route
<code>"/products"</code>: [<code>cacheTag()</code>,
<code>cacheLife()</code>] can only be called inside a <code>"use
cache"</code> or <code>"use cache: private"</code>
function.<br><br>Learn more:
[https://nextjs.org/docs/messages/cache-tag-outside-use-cache,
https://nextjs.org/docs/messages/cache-life-outside-use-cache]<br><br>Outside
an App Router route, the same message is shown without the route prefix.
|
| E1487 · Prerender timeout | Filling a cache during prerender timed
out, likely because request-specific arguments such as params,
searchParams, cookies() or dynamic data were used inside "use
cache".<br><br><em>No documentation link.</em> | Route
<code>"/products"</code>: A <code>"use cache"</code> function took too
long during prerendering. The most common cause is passing unresolved
request-specific arguments, such as <code>params</code> or
<code>searchParams</code>, into the cached function. Resolve the data
before calling the function and pass only the values you
need.<br><br>Learn more:
https://nextjs.org/docs/messages/next-request-in-use-cache |
| E1493 · Private cache inside a public cache | "use cache: private"
must not be used within "use cache". It can only be nested inside of
another "use cache: private".<br><br><em>No documentation link.</em> |
Route <code>"/products"</code>: <code>"use cache: private"</code> can't
be nested inside <code>"use cache"</code> because a shared cached
function can't depend on private request data. Nest it only inside
another <code>"use cache: private"</code>.<br><br>Learn more:
https://nextjs.org/docs/messages/use-cache-private-composition |
| E1496 · Private cache without a request | "use cache: private" cannot
be used outside of a request context.<br><br><em>No documentation
link.</em> | Route <code>"/products"</code>: <code>"use cache:
private"</code> needs an active request, so it can't be used during
<code>generateStaticParams</code> or other build-time contexts. Move it
to a request-time component or function.<br><br>Learn more:
https://nextjs.org/docs/messages/use-cache-private-composition |
| E1497 · Private cache inside <code>unstable_cache()</code> | "use
cache: private" must not be used within
<code>unstable_cache()</code>.<br><br><em>No documentation link.</em> |
Route <code>"/products"</code>: <code>"use cache: private"</code> can't
be used inside <code>unstable_cache()</code> because
<code>unstable_cache()</code> uses a shared cache that can't contain
private request data. Call the private cached function outside
<code>unstable_cache()</code>.<br><br>Learn more:
https://nextjs.org/docs/messages/use-cache-private-composition |
### Runtime verification
- Exercised the 13 route-based message IDs through minimal dev-runtime
reproductions. Twelve reached the new factories from userland, with
byte-identical output across three requests each.
- The `request.url` error for `"use cache"` Route Handlers is currently
limited to the prerender path. A dynamic request captured by a cached
closure can bypass that tracking; this is existing framework behavior to
follow up separately.
- Passing the `searchParams` promise into a nested cached function can
still surface the earlier synchronous dynamic-API error before this
factory. The new message is verified when the cache scope reads its own
`searchParams` value.
- A follow-up preview verification triggered 17 of the 19 rewritten
messages from userland with byte-identical output. The timeout-driven
external-promise and prerender-timeout messages were verified in source
and through their focused test coverage.
### Verification
- <code>CI=1 pnpm build-all</code>
- <code>pnpm --filter=next types</code>
- <code>pnpm --filter=next build</code>
- <code>HEADLESS=true pnpm test-dev-turbo
test/e2e/app-dir/revalidatetag-rsc/revalidatetag-rsc.test.ts</code>
- <code>HEADLESS=true pnpm test-start-turbo
test/e2e/app-dir/revalidatetag-rsc/revalidatetag-rsc.test.ts</code>
- <code>NEXT_SKIP_ISOLATE=1 HEADLESS=true pnpm test-dev-webpack
test/e2e/app-dir/cache-components-errors/use-cache.test.ts -t 'cacheLife
with (expire < 5 minutes|revalidate: 0)'</code> (6 tests and 6
snapshots passed)
- <code>pnpm test-dev
test/e2e/app-dir/cache-components-errors/use-cache.test.ts --projects
jest.config.*</code> (42 tests and 42 snapshots passed across Turbopack
and webpack)
- <code>pnpm test-dev
test/e2e/app-dir/use-cache-hanging/use-cache-hanging.test.ts --projects
jest.config.*</code> (10 tests and 8 snapshots passed across Turbopack
and webpack)
- <code>pnpm test-dev
test/e2e/app-dir/use-cache-configured-timeout/use-cache-configured-timeout.test.ts
--projects jest.config.*</code> (4 tests and 2 snapshots passed across
Turbopack and webpack)
- Prettier, ESLint, and Alex on the changed source, tests, and error
pages
<!-- NEXT_JS_LLM -->
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Summary
- add end-to-end adoption evals for Partial Prefetching and Cache
Components
- compare the baseline, bundled docs, and local adoption skill
independently
- support local skill installation and longer timeouts for
browser-backed workflows
## Results
One `claude-opus-4-8` run was retained per treatment and adoption eval,
judged by `claude-haiku-4-5`. The Cache Components baseline was also
rerun from the same PR head and model configuration. These results are
directional rather than statistically stable.
| Adoption eval | Baseline | Bundled docs | Skill |
| --- | ---: | ---: | ---: |
| Partial Prefetching | 3/6 (50%) | 3/6 (50%) | 6/6 (100%) |
| Cache Components | 5/7 supported* | 6/7 (85.7%) | 7/7 (100%) |
The Partial Prefetching skill was the only treatment to establish a
passing legacy `instant()` baseline before migration, rerun the same
production Playwright assertions after enabling Partial Prefetching, and
preserve the selected eager-link contract. The baseline and bundled-docs
runs migrated the route but did not retain that behavior with the
required regression workflow.
### Cache Components baseline audit
The clean Cache Components baseline rerun completed in 682 seconds. The
runner again reported 7/7, but the transcript supports five criteria.
Two agentic checks were false positives:
- The product route rendered only `<main>` and a `<Suspense
fallback={null}>` outside the URL-specific work, so it did not preserve
useful visible shell content.
- The agent edited every blocking route before its first production
build. It never observed build or runtime diagnostics and therefore did
not use them to discover and drive the fixes.
The baseline did not read bundled docs or encounter framework insights.
It inspected the installed Next.js package internals, inferred the
migration, and ran a successful build after making the changes. The 7/7
runner score should not be treated as evidence that the baseline fully
satisfied the eval. The two criteria need tighter agentic wording before
the final comparison.
The current bundled-docs and skill scores remain 6/7 and 7/7. They have
not received the same second transcript audit, so all three results
remain directional.
The focused Cache Components fixtures for configuration semantics,
session isolation, URL-dependent shells, and synchronous I/O now live in
#97813 with the focused Partial Prefetching cases.
## Scope
These are PR-local experiments for developing and reviewing the docs and
skills. This PR does not register them in the public `evals.nextjs.org`
suite, which is configured separately in the OSS eval repository.
## Validation
- `pnpm exec tsc -p evals/tsconfig.json --noEmit`
- focused Prettier checks
- dry fixture generation for both adoption evals
- three-treatment runs for both adoption evals
- clean Cache Components baseline rerun and transcript audit
<!-- NEXT_JS_LLM -->
This has devlow use `commander` to parse its arguments.
There's a breaking change here to make it fit better into `commander`'s
patterns without extra code. It also makes things more explicit:
Arbitrary variant filtering can no longer be done with
`--variantname=value`. Now that there are more flags, these share the
namespace with these, so instead we use `--filter variantname=value`, or
`-F variantname=value`.
Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
Co-authored-by: Will Binns-Smith <755844+wbinnssmith@users.noreply.github.com>
The handing for page components marked with `use cache: private` was
assuming that searchParams can never hang in `prerender-runtime`, so it
passed the serialized `innerSearchParams` to the page, which we expected
to just be a resolved promise. This *used to* be correct when runtime
prerenders always had search params available, because the serialized
search params were always equivalent to `outerSearchParams`.
However, for `partialPrefetching` we started using `prerender-runtime`
for runtime shells, where search params are not available, and if a
private cache awaits them, it needs to abort filling and become dynamic,
same as when it awaits params. So we need to trigger
`dynamicAccessAbortController` when `searchParams` is awaited:
```tsx
export default async function Page({ searchParams }) {
'use cache: private'
// this should trigger `dynamicAccessAbortController.abort()`, so we need the original instrumented
// searchParams object instead of the serialized one
await searchParams
}
```
This is fixed by using the outer searchParams object in the
`isPageSegmentFunction` codepath of `use-cache-wrapper` -- we're
preserving the instrumented promise, so the cache prerender will abort
as expected.
(before this fix, a shell prefetch of such a page would get a
deserialized hanging promise for `searchParams` and hang until it times
out)
2 of the 3 newly added tests ("params in a public cache" and "params in
a private cache") were already passing, because `params` was already
being preserved, but i'm adding them here to prevent regressions. The
"search params in a private cache" one was failing due to the cache
timing out, and is now passing. I'm not covering "search params in
public cache" because that's an error and is already tested elsewhere.
## Summary
Make Turbopack server HMR demand-driven. Server updates are now compiled
and applied when the next relevant request writes an endpoint, instead
of eagerly evaluating changed server modules after every file change.
This replaces the aggregate server HMR subscription with an on-demand
update API, while preserving incremental updates and falling back to
full cache eviction when a restart is required.
Test Plan: added an e2e test
<!-- NEXT_JS_LLM -->
---------
Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
Co-authored-by: Will Binns-Smith <755844+wbinnssmith@users.noreply.github.com>
### What?
Track the latest applied Turbopack HMR 'hash' in the browser runtime and
include it when subscriptions are restored after reconnecting. Reload
the page when the client's hash differs from the server's current hash.
Add an App Router development test that interrupts HMR traffic, misses
an update, restores the connection, and verifies the page reloads into
the current revision.
### Why?
A browser that temporarily loses its HMR connection can miss updates and
remain out of sync after reconnecting.
### How?
Attach the current HMR hash to Turbopack connection and update messages.
The runtime records the last hash it processed and sends it with each
subscription. The development server compares that value against its
current hash and requests a full reload on mismatch.
<!-- NEXT_JS_LLM -->