Commit Graph

1257 Commits

Author SHA1 Message Date
dan d792fcf5e7 Fix use cache over- and under-invalidation in dev (#96235)
In dev, `"use cache"` entries are keyed by an HMR refresh hash so that
edits invalidate cached data.

This hash was managed wrong in both directions:

- webpack refreshed the hash at moments when no code changed: on dev
server startup, and when a page file was added or removed. Each refresh
threw away all cached data for nothing.

- The hash never distinguished one dev server run from another. That
matters with a custom cache handler that persists entries: a restarted
server would serve data cached by the previous run, whose code may have
changed while the server was down.

To fix this:

- Adding or removing a page no longer refreshes webpack's hash. Edits
and env file changes still do.
- The hash now includes a random per-run value.

This fixes the `cache-components-dev-warmup` flakes.

## How to reproduce

Here's the first problem in action, on webpack. Render a cached
`Math.random()` on a page, then create an unrelated page file:

```
value: 0.199426
value: 0.199426
$ echo '...' > app/unrelated/page.tsx    # never visited
value: 0.990973                          # cached entry gone a second later
```

Adding the page threw the cached data away.

Starting the dev server throws it away too, once, shortly after boot.
That one is a race: normally the cache is still empty when it fires, so
nobody notices — but if the first page request gets in before it,
everything that request cached is thrown away and the next reload is
silently cold. That race is the cause of the `dev-warmup` flakes on
loaded machines, and it's how I found this.

The hash refresh is specific to webpack, so Turbopack doesn't lose
cached data this way. On Turbopack, these events instead clear the
server's module cache. Whether the next render picks up a fresh module
instance is a race, so module-scope state can reset. Turbopack also has
a separate bug in this area — it advances its hash once per
change-subscription emission, and an added or removed page can produce
an emission without any edit — which #96248, stacked on this PR, fixes
by deriving the hash from compiled output.

## Underlying cause

Next computes a "client router filter" (a bloom filter over the app's
routes, used by the client-side router), and a filter change is treated
like an env file change. An env change means cached data might be stale,
so the reaction is strong: the browser is told to re-fetch, and on
webpack the hash is refreshed, which discards every cached entry. But
the filter is derived from the routes, so it "changes" whenever you add
or remove a page — and once right after startup, because the first
computation has nothing to compare against. Neither makes cached data
stale.

The second problem is the opposite: dev server runs share cache keys.
The hash starts from the same value on every run (unset on webpack,
`"0"` on Turbopack), so a restarted server generates the same keys as
the old one. With the default in-memory handler that's moot, but a
custom cache handler that persists entries will serve entries written by
the previous run. webpack can also hit this after edits: its hash is
derived from compiled output, so re-applying an edit that a previous run
also made lands on that run's keys.

Judging by the code (?), dev cache reuse was only ever meant to last a
single run: every dev cache mechanism is in-memory, and #75474 takes it
for granted that restarting the dev server clears cached content. So I
treat cross-run reuse as a bug. Not sure I got this constraint right —
in particular, #75474 deliberately made reverting an edit reuse
previously cached data, and this PR keeps that working within a run but
stops it from working across runs.

## Fix

Two changes:

- A filter change is no longer treated as an env change. It still
updates the compiled-in filter value (the part that's actually needed),
but it no longer refreshes the hash, and on Turbopack it no longer wipes
the server's module cache. Real env file changes still trigger the full
reaction.

- The hash now starts from a random per-run value on both bundlers. So
it exists before the first request and never matches another run's keys.

The per-run value is deliberately conservative: it also prevents reuse
across restarts where *nothing* changed. Allowing exactly that reuse
safely needs keys derived from the cached function's implementation (the
existing TODO in `use-cache-wrapper.ts`), which would replace the
per-run value.

## Test plan

The first commit adds five tests
(`test/development/app-dir/cache-components-spurious-cache-invalidation`).
They run on both bundlers, except the module-state test, which is
Turbopack-only: on webpack, adding a page recompiles the server bundle,
which replaces the module instances regardless. So Turbopack runs five
and webpack four.

Before the fix, these fail:

```
webpack:    ✕ discards use cache entries when a previous run made the same edit
            ✕ discards use cache entries across dev server restarts   (most runs — see below)
Turbopack:  ✕ keeps module state when an unrelated page is added
            ✕ discards use cache entries across dev server restarts
```

The webpack restart test does not fail every run. The start-time refresh
replaces the unset hash with a content-derived one at an unpredictable
moment. When one server run caches before its refresh and the other
reads after its own, the keys don't collide, so there is no cross-run
reuse for the test to catch that run.

The webpack side of the over-invalidation fix is covered by the refetch
count below rather than by a cached-value test: the equivalent test on
Turbopack depends on emission timing, so it lands with #96248, which
removes that dependence.

The same commit also tightens the refetch count in the test from #96250
below: since a page add is no longer an env change, adding a page
refetches an open tab once instead of twice, on both bundlers. Before
the fix, both bundlers fail that expectation (`Expected: 1, Received:
2`).

To reproduce the flake this PR fixes, run the dev-warmup suites under
CPU load (`for i in $(seq 1 12); do yes > /dev/null & done`). Before the
fix, 6–8 of the 11 tests fail per run on webpack (`Prerender` flips to
`Server` on warm reloads). After the fix, all 11 pass. Turbopack passes
before and after. Also still green: `typed-env` (env definitions were
previously only written at boot via the false positive this removes; now
they're written on the initial scan explicitly), `env-config`,
`use-cache-custom-handler`, and `pages-to-app-routing` (it exercises the
router filter update path).
2026-08-06 14:25:52 +01:00
dan f58c669ab2 Fix which pages the dev server announces, and when (#96250)
The dev server tells open tabs when a page is added or removed, so a tab
showing a 404 picks up a page you just created, and a tab showing a page
you just deleted falls back to the 404.

Both bundlers computed the list of changed pages wrong, in opposite
directions.

**Turbopack announced pages that hadn't changed.** To find what changed,
it compared the new route list against the entry maps in
`currentEntrypoints`. Those maps key App Router entries by page name
(`/blog/page`), but the route list is keyed by route (`/blog`), so no
App Router route ever matched: every update announced every existing app
route as added and every page name as removed, and starting the server
announced every route once. Each announcement makes every connected tab
refetch. Fixed by comparing the new route list to the previous route
list, which uses the same naming. Starting the server now announces
nothing on either bundler, since the first route list is not a change.

**webpack didn't announce pages that had changed.** It only announced
when `prevSortedRoutes.every((val, idx) => val === sortedRoutes[idx])`
was false. That is a prefix comparison: when a new route sorts after all
existing ones, the old list is a prefix of the new one, and nothing is
announced. In App Router this mostly went unnoticed, because adding a
page was also (wrongly) treated as an env change, which refreshed tabs
anyway; the next PR in this stack removes that. In Pages Router nothing
hid it: the route manifest was never refetched, so client-side
navigation to the new page kept returning 404 until a manual reload.
Fixed by also comparing the lengths.

**What this PR does not fix:** on Turbopack there is a window right
after a page is added where the dev server has already announced it to
tabs, but can't serve it yet (the route isn't in the dev router's route
table until the watcher pass in `setup-dev-bundler` runs). A tab that
reacts inside that window gets the 404 again, and since each page is
only announced once, it stays on the 404 until you reload it by hand.
That bug predates this PR and is being fixed separately.

So that the new App Router tests don't hit this window, each one edits
the added page again once it's confirmed servable. The edit makes the
server announce again, and this time the tab can only get the page. The
trade-off is that these tests don't prove the first announcement alone
updates the tab — the refetch-count test covers how often changes are
announced, and its tab never sits on the added page's 404, so it can't
hit the window. A TODO in the tests says to drop the extra edits once
the underlying bug is fixed. The Pages Router test needs no such edit:
Turbopack updates the route matchers before it announces, and a Pages
Router tab reloads the document instead of refetching.

## Test plan

New tests cover adds, removals, dynamic routes, route groups, and a
dynamic route being shadowed by a more specific page and then
unshadowed. One more counts how often an open tab refetches when a page
is added: it wraps the tab's `fetch` and counts requests carrying the
`next-hmr-refresh` header, which distinguishes a refetch the dev server
asked for from a navigation or a prefetch. It runs on a dev server no
other test shares, because the count depends on what has been compiled
so far.

Before the fix, adding one page makes an open tab refetch:

```
Turbopack: 12 times   (every app route re-announced; grows with the number of routes)
webpack:    1 time    (the announcement is never sent; only the env-change refetch happens)
```

The correct count is currently 2: one refetch for the announcement, one
for the env-change false alarm. The next PR removes the false alarm and
tightens the expectation to 1.

Before the fix, webpack also fails the Pages Router manifest test (the
manifest update is sent from the same skipped branch).

With the fix, both bundlers pass all ten tests.
2026-08-06 14:25:52 +01:00
Sebastian "Sebbie" Silbermann 2c04735944 [fragment-scroll] Remove config.experimental.appNewScrollHandler (#95602) 2026-08-06 09:53:09 +00:00
Luke Sandberg ff3a2cfaa9 [turbopack] Strip leading BOM before parsing CSS (#96678)
Fork PR #96379 by @lazerg, re-opened as a branch PR so the "when
deployed" CI jobs can run — those require Vercel deployment secrets that
GitHub does not expose to pull requests from forks, so they can never
pass on the original.

**The fix commits are unchanged and still authored by @lazerg.** This PR
only adds tests on top. Please credit them; #96379 should be closed in
favor of this one.

### What?

A CSS file beginning with a UTF-8 BOM (`EF BB BF`) is mishandled by
Turbopack. Lightning CSS does not skip the BOM, so it is tokenized as
content and the first token is misparsed:

```
./app/bom.css:1:2
Error: Parsing CSS source code failed
Unexpected token AtKeyword("layer")
```

The user-visible symptom is broader than a failed build. Turbopack
parses with `error_recovery: true`, and under that setting a leading BOM
makes Lightning CSS return `Ok` with **zero rules** — so a BOM-prefixed
stylesheet could silently drop all of its styles instead of erroring.
dart-sass (compressed style) and PostCSS >= 8.5.24 both emit or
round-trip such a BOM, so real projects hit this.

Fixes #96374

### How?

Strip a leading `U+FEFF` in `parse_css_stylesheet` before handing the
source to Lightning CSS, covering both `StyleSheet::parse` call sites
while leaving `ParseCssResult.code` as the original bytes that code
frames are rendered from.

That split makes parser positions relative to the stripped copy while
code frames still render the original line, so first-line positions need
compensating. `source_pos_for_loc` adds the stripped character back for
line 0 of BOM files. Only line 0 is affected, because the BOM contains
no newline.

### Tests

`test/e2e/app-dir/css-bom` — a BOM-prefixed stylesheet compiles and its
rules reach the page. Verified failing without the fix with the exact
error above, and passing with it, in dev-turbo, start-turbo and
start-webpack.

`test/development/app-dir/css-bom-code-frame` — covers the position
correction. Two fixtures hold the same invalid `@media (min-width: {})`
on line 1 and differ only by the leading BOM; the test asserts the BOM
file's reported column is exactly one greater:

| | `no-bom` | `bom` | |
|---|---|---|---|
| without `source_pos_for_loc` | 18 | 18 | fails |
| with it | 18 | 19 | passes |

Asserting the relationship rather than a literal column keeps this
robust if Lightning CSS changes its absolute column convention. It is
kept separate from the e2e suite because the fixtures are intentionally
invalid CSS, and scoped to Turbopack in dev, where the warning reaches
the CLI as the page is requested.

---------

Co-authored-by: lazerg <lazerg2@gmail.com>
Co-authored-by: vercel-gh-bot-3[bot] <282332853+vercel-gh-bot-3[bot]@users.noreply.github.com>
2026-08-04 21:25:04 +00:00
Marcos Hernanz 39b7da2ee8 Turbopack: terminate failed plugin worker threads (#96592)
## What

Terminate a worker-thread plugin runtime when an evaluation or IPC error
marks
its `WorkerOperation` non-reusable.

`disallow_reuse()` already removed the worker from pool statistics and
disabled
the callback that returns it to the pool, but it did not stop the
underlying
Node.js `Worker`. The worker then stayed strongly owned by
`loaderWorkers`, kept
its V8 isolate/module graph alive, and waited forever for another task
that
could never arrive.

This calls the existing worker terminator used by `wait_or_kill`,
scale-down,
and scale-to-zero. It removes the routed native channel and dispatches
the
existing JavaScript `Worker.terminate()` callback, which also deletes
the map
entry.

## Why this matters for v0

v0's planned worker-thread runtime turns transient loader/PostCSS errors
during
agent edits into a deterministic lifecycle leak. On the real v0
`/button`
route, each unique Tailwind/PostCSS failure retained one complete worker
runtime
on the baseline.

The balanced eight-process A/B panel (80 unique failures per lane)
measured:

| Lane | `WorkerThread` slope | Private-memory slope | Median failure |
Median recovery | >5 s failures |
|---|---:|---:|---:|---:|---:|
| baseline | **+1.000/error** | **+49.81 MiB/error** | 108.04 ms |
329.26 ms | 3 / 80 |
| candidate | **0.000/error** | **+5.12 MiB/error** | 108.15 ms | 317.65
ms | 2 / 80 |

That is a **100% elimination of leaked-worker growth** and an **89.7%
reduction
in retained private-memory slope** (exact process-bootstrap 95%
interval:
85.7%–99.5%). Every candidate process was below every baseline process;
exact
4-vs-4 process-label permutation `p=0.0143` one-sided / `0.0286`
two-sided.
Median failure latency changed by only +0.11 ms.

The independent 100-cycle-per-lane endurance confirmation showed the
same
causal result:

| Lane | `WorkerThread` cycle 1→100 | Private memory cycle 1→100 |
Private slope | Median failure | Median recovery |
|---|---:|---:|---:|---:|---:|
| baseline | **38→137** | **4.28→7.34 GiB** | **+27.02 MiB/error** |
104.45 ms | 331.32 ms |
| candidate | **32→32** | **3.93→4.10 GiB** | **+0.98 MiB/error** |
102.94 ms | 297.24 ms |

That long run eliminated all observed worker growth and reduced the
fitted
private-memory slope 96.4%. Every one of the 200 unique failures
recovered to
the complete real route, and the source hash was restored after every
cycle.

The benchmark used fresh `.next` state per process, five error/recovery
warmups,
a five-minute quiescence window, unique uncached errors, exact source
restoration
after every cycle, and within-process slopes. Baseline and candidate
native
binaries were built from the same Next.js base, with only this lifecycle
change
affecting candidate native code, and selected by SHA-verified
`NEXT_TEST_NATIVE_DIR` paths.

This is independent of #96433. That PR coordinates explicit multi-file
edit
transactions; this PR fixes the lifecycle of a worker that has already
failed.
It also does not change idle-worker scale-down policy: failed workers
are absent
from the idle pool, so scale-down cannot see them.

## Regression test

The new development test schedules a one-second filesystem marker inside
a
custom loader, then throws an evaluation error under
`turbopackPluginRuntimeStrategy: 'workerThreads'`:

- baseline native binary: fails because the orphan worker remains alive
and
  writes the marker;
- candidate native binary: passes because the worker is actually
terminated;
- candidate then renders a recovered value, proving replacement/recovery
works.

## Validation

- `pnpm build-all`
- `cargo fmt --all -- --check`
- `cargo check -p turbopack-node --all-targets`
- `cargo test -p turbopack-node --lib`
- `cargo clippy -p turbopack-node --all-targets -- -D warnings`
- focused Prettier and ESLint checks for the new test
- candidate integration test pass; exact baseline integration test fails
at the
  intended liveness assertion
- GPT-5.6 Sol xhigh + Claude Opus 5 xhigh autoreview panel: zero
actionable
  findings, patch correct at 0.96 confidence

Exact optimized native SHA-256 values used by the benchmark:

- baseline:
`609eeeed41e1425f06136b54b7997e5b1b1add992169b17a797889dfb830dd7f`
- candidate:
`141211112ae47b8bd814aaab437060114e92f60a1bf6f23e1b0e153b1e4837cc`

Both stripped binaries were 205,863,784 bytes. Raw per-process JSON,
`/proc`
samples, server logs, harness source, analysis output, and A/B plus
endurance
graphs are retained in the investigation archive.

Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>
2026-08-04 13:29:48 -07:00
Tim Neutkens cbf0cef687 Enable TypeScript CLI by default (#96497)
## Summary

Enable the project-local TypeScript CLI checker by default during `next
build`, while preserving `experimental.useTypeScriptCli: false` as an
opt-out to the TypeScript compiler API. Update the TypeScript 7
guidance, diagnostics, tests, and documentation to match the new
default.

## Verification

- `pnpm exec jest --runTestsByPath test/unit/isolated/config.test.ts`
- Pre-commit lint-staged checks passed (Prettier and ESLint)
- Not run: `pnpm test-start-turbo
test/production/app-dir/typescript-cli/typescript-cli.test.ts` (isolated
fixture dependency installation was blocked by unavailable npm registry
access)
- Not run: `pnpm --filter=next types` (repository has pre-existing
unrelated TypeScript errors)

<!-- NEXT_JS_LLM -->
2026-08-03 18:10:51 +02:00
Sebastian "Sebbie" Silbermann 7612eaeda1 Consolidate unhandled rejection logging into a single listener (#95999)
An unhandled rejection was previously logged by up to three independent
process listeners at once: the render runtime's crash-prevention handler
in `process-error-handlers.ts` (a bare `console.error`), the router
server's `Log.error('unhandledRejection: ', err)`, and the dev server's
`logErrorWithOriginalStack`. The runtime handler must exist on every
deployment target (#77997), but on self-hosted `next start`/`next dev`
it shares a process with the router server's and dev server's listeners,
so a single rejection was logged multiple times in different formats.

THe first commits adds a test showing the current behavior where we log
multiple times.

The second commit introduces `registerUnhandledRejectionListener` and
`isUnhandledRejectionListenerRegistered` in `process-error-handlers.ts`,
and converts the router server and dev server to check-then-register
instead of installing their own rejection loggers:

- The listener function is shared via a `Symbol.for` key on
`globalThis`, so multiple copies of the module (e.g. in the pre-compiled
server bundle and in a route module bundle) register and detect a single
listener instance.
- The registration check queries
`process.listeners('unhandledRejection')` instead of a module-global
flag, so it stays accurate even after external code calls
`process.removeAllListeners`. `installProcessErrorHandlers` therefore
calls the register function unconditionally.

The `uncaughtException` handlers are left as they are; they have the
same duplication and could be consolidated the same way in a follow-up.
2026-07-31 11:36:40 -04:00
Will Binns-Smith 466cdfaa27 Turbopack server hmr: avoid complete clear on graph changes (#95546)
Currently, importing a new module causes a complete eviction (`clear()`)
and re-evaluation of server chunks, both in the Turbopack runtime's
module cache and Node's `require.cache`.

Here's what happens today:

- A new module is imported into a chunk's graph
- This changes its `availability_info`, which in dev for non-entry
chunks is encoded into the chunk's filepath
- `VersionedContentMap` works entirely on chunk paths. When we construct
instructions to transition a chunk into its new state, we fail to find
the prior state and fall back to `clear()` as described above.

An ideal version of this is a refactor that implements
`VersionedContentMap` on a per-module basis, not a per-chunk one. This
PR doesn't do that, but achieves consistent module-level updates when a
chunk's availability info changes.

It does this by implementing chunk lists for server entry chunks the
same way the client hmr implementation does: an entry chunk's version
aggregates the versions of its dependent chunks (including dynamically
imported ones), keyed by merger rather than path. Entry chunks do not
encode availability info in their paths, so they are insulated from
missing versions. Updates are applied in Node.js through the same shared
merged-update machinery in the unified hmr runtime that the client
already uses, plus a new `ChunkListUpdate` branch in the server hmr
client to unwrap the merged updates.

Details:

- The merged-update wire format (`EcmascriptMergedUpdate` etc.) moves
from turbopack-browser into shared `turbopack_ecmascript::chunk_list`,
along with runtime-agnostic `ChunkListVersion` and `update_chunk_list`
implementations.
- `turbopack-nodejs` gains a chunk content merger mirroring the browser
one. `EcmascriptBuildNodeEntryChunk`'s versioned content is now a
chunk-list content over its sync and async chunks. It is version-only
and never emitted; the entry chunk still inlines its loader calls.
- The aggregate server hmr subscription now tracks only entry chunks.
Shared chunks under `server/chunks/` ride the entry's `ChunkListUpdate`
as module deltas, so their content-hash paths no longer matter.
- The Node hmr client applies `ChunkListUpdate` by feeding each nested
merged update through the shared apply path. Modules that appear in an
"added" chunk but already exist in the module cache were moved by a
chunk rename and are treated as modified.
- On a successful partial apply, the hot reloader clears the manifest
cache for updated chunks and notifies browsers to refetch RSC, without
clearing `require.cache`.

### Test Plan

Adds a series of e2e tests to catch the described manual test plan in
both entry and dynamic chunks.
2026-07-28 13:49:30 -07:00
David Alexandru Ilie 925edc3dd6 Classify Instant Insights as internal in Request Insights (#96147)
## Summary

- Classifies Instant Insights records as internal and hides them by
default.
- Adds a persisted “Internal activity” toggle.
- Nests internal records below their matching foreground request.
- Preserves orphaned internal records exactly once.
- Signals hidden internal errors without littering the main request
list.
- Raw endpoint and CLI data remain complete; filtering is UI-only.

## Verification

```bash
pnpm build-all
pnpm --filter=next types
pnpm --filter=next build
pnpm test-unit --runTestsByPath packages/next/src/next-devtools/dev-overlay/components/request-insights/trace-viewer.test.ts
pnpm test-dev-turbo test/development/app-dir/request-insights/request-insights.test.ts
pnpm test-dev-webpack test/development/app-dir/request-insights/request-insights.test.ts
```

<!-- NEXT_JS_LLM -->
2026-07-28 14:15:21 -04:00
David Alexandru Ilie c294cb7539 Persist Request Insights display settings (#96277)
## Summary

- Replaces the per-request verbose checkbox with a global Request
Insights settings menu.
- Persists `requestInsights.verbose` through the existing DevTools
configuration path.
- Restores the setting across reloads.
- Does not include the unrelated global `pagehide` flush behavior.

## Verification

```bash
pnpm build-all
pnpm --filter=next types
pnpm --filter=next build
pnpm test-dev-turbo test/development/app-dir/request-insights/request-insights.test.ts
pnpm test-dev-webpack test/development/app-dir/request-insights/request-insights.test.ts
```

<!-- NEXT_JS_LLM -->
2026-07-28 14:15:21 -04:00
Tim Neutkens efcbb27450 Add Instant Insights pipeline spans (#95961)
## Summary

- Adds a detached `Instant Insights` root span.
- Surfaces `Prepare validation inputs` and `Run validation` as child
phases.
- Covers both the validation worker and Webpack’s in-process path.
- Stores these spans separately from the foreground request.
- Preserves scheduling, cancellation, and concurrent-document behavior.

## Verification

```bash
pnpm build-all
pnpm --filter=next types
pnpm --filter=next build
pnpm test-unit --runTestsByPath packages/next/src/next-devtools/dev-overlay/components/request-insights/trace-viewer.test.ts
pnpm test-dev-turbo test/development/app-dir/request-insights/request-insights.test.ts
pnpm test-dev-webpack test/development/app-dir/request-insights/request-insights.test.ts
pnpm test-dev-turbo test/development/app-dir/instant-validation-scheduling/instant-validation-scheduling.test.ts
```

<!-- NEXT_JS_LLM -->

---------

Co-authored-by: David Ilie <david@davidilie.com>
2026-07-28 14:15:21 -04:00
Andrew Clark 3de2d1a213 Unify allow-runtime with Partial Prefetching (#96106)
Removes the "allow-runtime" prefetch config, and turns its behavior on
implicitly wherever Partial Prefetching is enabled.

The original motivation for "allow-runtime" was to give apps more
control over server costs triggered by prefetches. Until a route
explicitly opts in, prefetches would only be served from the CDN, not
from the server. The problem, though, was it was very confusing to know
when to add or remove this configuration. The incentive for many apps
was to add it everywhere, with no clear signal for when to remove it.

Our updated thinking is that Partial Prefetching itself already provides
sufficient protection against runaway prefetching costs: per-link
prefetches only happen on Link components that explicitly opt in with
the prefetch prop.

The optimizations landed earlier in this stack also make allow-runtime
less necessary: on pages where all the content is statically renderable,
prefetches are served from the static cache and no runtime request is
ever issued; only a page that accesses non-static data is prefetched at
runtime.

The upshot of this decision is that runtime versus static becomes an
internal optimization; the same content gets prefetched regardless of
whether or how Next.js is able to optimize it.
2026-07-28 11:51:29 -04:00
David Alexandru Ilie a619237b1c Use a safe clock for Request Insights bookkeeping (#96274)
Draft replacement for #96267 after restructuring the Request Insights
stack.

## Summary

- Request Insights bookkeeping used `Date.now()`, which Cache Components
patches to detect unstable application wall-clock reads.
- Framework bookkeeping could therefore be incorrectly attributed to
user code.
- Implicit timestamps now use `performance.timeOrigin +
performance.now()`.
- Explicit OpenTelemetry timestamp inputs remain unchanged.
- Tests distinguish framework bookkeeping from a genuine application
`Date.now()` call.

## Verification

```bash
pnpm build-all
pnpm --filter=next types
pnpm --filter=next build
pnpm test-unit --runTestsByPath packages/next/src/server/lib/trace/local-span-recorder.test.ts packages/next/src/server/lib/trace/span-store.test.ts packages/next/src/server/lib/trace/request-insights.test.ts
pnpm test-dev-turbo test/development/app-dir/request-insights/request-insights.test.ts
pnpm test-dev-webpack test/development/app-dir/request-insights/request-insights.test.ts
```

<!-- NEXT_JS_LLM -->
2026-07-27 18:58:54 -04:00
Tim Neutkens 31b78bbed9 Add Instant Insights request timing (#95958)
## Summary

- record deferred Instant Insights work as a separate typed Request
Insights item
- keep validation spans, fetches, timing, and live updates separate from
the foreground request
- label Instant Insights consistently in DevTools and CLI output
- stack this observability change on #95939, which moves validation off
the navigation response path

## Verification

- `pnpm jest --runInBand
packages/next/src/server/lib/trace/request-insights.test.ts
packages/next/src/server/lib/trace/local-span-recorder.test.ts
packages/next/src/next-devtools/dev-overlay/components/request-insights/trace-viewer.test.ts
packages/next/src/next-devtools/dev-overlay/shared.test.ts`
- `NEXT_SKIP_ISOLATE=1 NEXT_TEST_PREFER_OFFLINE=1 pnpm test-dev-turbo
test/development/app-dir/request-insights/request-insights.test.ts`
- `NEXT_SKIP_ISOLATE=1 NEXT_TEST_PREFER_OFFLINE=1 pnpm test-dev-webpack
test/development/app-dir/request-insights/request-insights.test.ts`
- `pnpm --filter=next types`

<!-- NEXT_JS_LLM -->

---------

Co-authored-by: David Ilie <david@davidilie.com>
2026-07-27 07:27:19 -04:00
dan b51ca60fd4 Fix dev overlay symbolication for project paths needing percent-encoding (#96221)
In a project whose absolute path contains characters that percent-encode
in URLs — e.g. a space — the dev overlay showed no code frame and raw
`file://` URLs instead of source locations for server frames.

Two encoding mismatches caused this:

- Turbopack's `trace_source` percent-decoded the source map's original
file URL before comparing it against the still-encoded project root URI,
so the containment check failed ("Original file ... outside project")
for any project path with such characters. The comparisons now stay in
the encoded domain and only the outputs are decoded back into paths.

- React synthesizes stack frame `file:` URLs by prepending the scheme to
a filesystem path, so the overlay receives URLs with e.g. raw spaces.
These aren't well-formed, and they don't match the `url.pathToFileURL`
form Node.js keys its source map cache by. The overlay middleware now
re-encodes them through WHATWG URL parsing, which tolerates such input.

## Test Plan

New tests.

Known cases that remain broken are commented out with a TODO.
These could be resolved if https://github.com/react/react/pull/37105 or
equivalent lands.

## Before

<img width="825" height="732" alt="Screenshot 2026-07-25 at 14 39 18"
src="https://github.com/user-attachments/assets/0f119e52-ec86-495a-bf5c-5339faa1d606"
/>


## After

<img width="1126" height="733" alt="Screenshot 2026-07-25 at 14 58 14"
src="https://github.com/user-attachments/assets/a263ee8a-4cfe-4358-9a8e-2e58b4b16aa5"
/>

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 18:36:45 +01:00
Hendrik Liebau e1fbb1800f Run dev validation in process when using Webpack (#96219)
Node.js caches source maps per isolate, so the dev validation worker
only has maps for the chunk files loaded on its own thread. For chunks
it never loads, such as a module behind a dynamic `import()`, it reads
the `.map` that Turbopack writes next to the chunk. Webpack keeps its
dev source maps in the compiler instead, so the worker has nothing to
read and reports those frames with the position they have in the
compiled output rather than in the source.

This stops using the worker when the bundler is Webpack, so validation
runs in process again, where those frames resolve, and
`experimental.devValidationWorker` now has no effect there. Dev
performance under Webpack goes back to what it was before validation
moved to a worker, which is the better trade: the worker exists to keep
the event loop responsive during rapid navigation, and that is not worth
losing the source location on validation errors.
2026-07-25 16:30:39 +02:00
Hendrik Liebau 4bfd5c7170 Read chunk source maps from disk in the dev validation worker (#96218)
Node.js caches source maps per isolate, so the dev validation worker
only has maps for the chunk files loaded on its own thread. It never
renders server components, and the built entry reaches segment modules
through lazy getters, so it loads whatever `loadComponents` pulls in and
nothing else. A module the bundler splits into a chunk of its own, as
Turbopack does for a dynamic `import()`, is therefore missing from that
cache entirely, and the frames pointing into it are logged without a
source location. In-process validation resolves them, so this is a
regression from moving validation onto a worker.

On the main thread the same miss is covered by asking Turbopack for the
map through the `Project` handle, which cannot cross a thread boundary.
The worker instead reads the `.map` that Turbopack already wrote next to
the chunk, which needs no project handle and, unlike Node's cache, does
not depend on the chunk having been evaluated on this thread. Lookups
are restricted to `distDir` so a stack frame cannot point the reader at
an arbitrary file, and both hits and misses are memoised.

Installing that lookup also required moving
`bundlerFindSourceMapPayload` onto a `globalThis` symbol.
`patch-error-inspect` is bundled into several runtimes that each get
their own copy, and the copy that registers the implementation, here the
worker bundle, is not the copy that symbolicates the frame, which is the
app-page bundle's. The code frame renderer in the same file is already
shared this way for the same reason.

Webpack keeps its dev source maps in the compiler rather than writing
them next to the chunks, so there is nothing for the worker to read and
its frames are unchanged. A follow-up turns the worker off for Webpack
so that validation runs in process, where those frames still resolve.
2026-07-25 16:30:38 +02:00
Hendrik Liebau c1617dedc8 Retry the source map lookup with a plain path (#96215)
Running
`test/e2e/app-dir/instant-validation/suspense-boundaries.test.ts`
locally on Node.js 24 currently fails in the two "missing suspense
around search params" cases: stacks that should read
`app/…/page.tsx:40:18` are logged as raw
`about://React/Prefetch/file:///…/%5Broot-of-the-server%5D…` URLs, so
the validation errors printed to the terminal no longer point at any
source. CI is green on the same commit, because it runs Node.js 20. The
dev validation worker (#96153) and the switch to `file:` source maps for
Turbopack (#95946) landed hours apart, and the worker's branch predated
the source map change, so the two first met on canary, where the older
Node.js hid the result.

Symbolicating a frame that React spliced in from another environment
means decoding the chunk path out of its `about://React/…` URL and
asking Node.js for that chunk's source map. Decoding returns the path as
written, while Node.js keys its cache by the `pathToFileURL` encoding of
the same path, and from Node.js 22 on that encoding escapes the brackets
in Turbopack's `[root-of-the-server]` chunk names, so the lookup misses.
Node.js 20 leaves brackets alone, which is why the same code resolves
there.

PR #95946 anticipated the mismatch and left it as a follow-up, because
on the main thread a miss is only wasteful: Turbopack hands back the
chunk's map instead, which means parsing one the process had already
parsed and cached. The validation worker runs on its own thread with no
access to the Turbopack project, so there the miss is final. Node.js
also accepts the plain path as a cache key, unambiguously for both a
CommonJS absolute path and an ESM `file:` URL, so we retry with it
before giving up. Making React's fake frame URLs reversible, as proposed
in https://github.com/react/react/pull/37105, would let the first lookup
succeed on its own and retire the retry.

The test added here pins down what the worker can symbolicate at all. It
caches maps per chunk file in its own isolate and never renders server
components, so a statically imported module sits in a chunk it has
already loaded and resolves, while a dynamically imported one gets a
chunk of its own that is never loaded and does not. The snapshots for
that second route record broken output on purpose: in-process validation
resolves the frame, so losing it is a regression from moving validation
onto a worker, and it affects Turbopack (no location) and Webpack
(compiled positions) alike. Follow-ups address each.

The snapshots are recorded with the retry applied rather than before it.
Without it Turbopack's output differs between Node.js 20 and 22, and the
broken URLs embed the test's temporary install directory, which changes
on every run, so no stable recording exists. One consequence is that CI
cannot fail on a regression of the retry itself; that was checked by
running the new suite under Node.js 24.
2026-07-25 15:15:50 +02:00
Hendrik Liebau 3c4afc1a3a Run Cache Components dev validation on a worker thread (#96153)
This moves the dev-mode Cache Components validation renders off the dev
server's main thread onto a worker thread, so rapid navigation no longer
starves the event loop. The worker crosses into the app-page bundle
exactly once, calling a new
`ComponentMod.routeModule.runValidationInDev` entry that rebuilds the
render context, work store, and request store from a serializable
snapshot and runs the whole validation there, so the client prerender
and the user's client components resolve the single app-page React
instance rather than a second copy.

A thin worker shell (`dev-validation-worker.ts`) plus a single-worker
pool (`dev-validation-worker-pool.ts`) load the user bundle via
`loadComponents`, install code-frame support for CLI output, and forward
the validation errors back as Flight bytes; the main thread only
delivers them to the overlay through `sendErrorsToBrowser`. The
snapshot, the globalThis-symbol handoff, and the shared error-delivery
helpers live in their own modules. The dev server installs the worker
when `experimental.devValidationWorker` is not `false`, and
`runDevValidationInBackground` uses it when present, falling back to the
in-process path otherwise. A one-slot `SharedArrayBuffer` propagates a
supersede abort into the worker so a newer navigation cancels an
in-flight validation.

The runtime bundle gains an `app-worker` entry for the worker with the
`build/swc` boundary externalized for the code-frame native binding, and
`patch-error-inspect.ts` now backs its code-frame renderer with a
globalThis symbol so all copies of the module share it across the
thread.

The synthetic `bench/dev-validation` benchmark navigates back-to-back,
so every click lands inside the validation window — the worst case for
main-thread contention. Browser-observed navigation TTFB in that window,
worker vs in-process:

| Route  | Worker (p50/p95/max) | In-process (p50/p95/max) |
| ------ | -------------------- | ------------------------ |
| client | 19 / 24 / 27 ms      | 40 / 66 / 7762 ms        |
| server | 42 / 45 / 46 ms      | 122 / 158 / 252 ms       |
| sprite | 109 / 117 / 169 ms   | 196 / 208 / 299 ms       |

The steady-state difference is only tens of milliseconds; the effect
that matters is the tail. In-process, a navigation that collides with an
in-flight validation render can stall for seconds (this run peaked at
~7.8s on the client route, and the peak varies run to run) because a
staged render does not yield until it finishes; off-thread the main
thread stays free and that stall disappears.

Read these as an upper bound, not a speedup that generalizes. The work
moved off-thread is the validation render's CPU — bounded,
route-dependent, and free of IO — so it does not grow with the main
render's cost. In an app whose main render is dominated by IO the same
absolute saving is a small fraction of the request, and it only appears
when a navigation lands in the brief validation window; at ordinary
click speed it is largely invisible. This is a dev-only responsiveness
improvement whose benefit varies widely with the app and the navigation
pattern.

As a follow-up, the validation work that still runs on the main thread
could move to the worker as well. When the main render can't be reused
for validation (for example after a cache miss), the validation
re-renders on the main thread to produce its inputs, resuming from the
Resume Data Cache (RDC) the main render already filled, and only then
hands the resulting Flight chunks to the worker. A later iteration could
run those renders inside the worker too, transporting the (serializable)
RDC so the worker can resume from the filled caches rather than reading
them back on the main thread.

closes NAR-895
2026-07-25 07:21:03 +02:00
Hendrik Liebau 62084e3fbd [test] Unflake the enabled-features-trace test suite (#96175)
<img width="428" height="179" alt="Screenshot 2026-07-24 at 22 42 04"
src="https://github.com/user-attachments/assets/38478940-d23e-4faa-bd88-5758190be158"
/>

[Flakiness
metrics](https://app.datadoghq.com/ci/test/runs?query=test_level%3Atest%20%40git.repository.id%3A%22github.com%2Fvercel%2Fnext.js%22%20%40test.name%3A%22enabled%20features%20in%20trace%20should%20denormalize%20inherited%20enabled%20features%20during%20upload%22%20%40test.type%3A%22nextjs%22%20%40test.status%3A%28%22fail%22%20OR%20pass%29&agg_m=count&agg_m_source=base&agg_t=count&fromUser=true&index=citest&start=1784320894753&end=1784925694753&paused=false)

The `render-path` span is recorded when a request's response closes,
which is too late for any flush other than the one the dev server
performs while shutting down. The parent `next dev` process escalates to
SIGKILL 100ms after signalling the child, and on a machine running eight
test files at once the child does not reliably get scheduled to run its
cleanup within that window, so the span never reached the trace file and
the upload assertions failed. This change raises the budget for the test
through `NEXT_EXIT_TIMEOUT_MS`, which was added alongside that timeout
in #67165 so that it can be increased when the child's exit work matters
more than a fast exit. The same approach is already used in
`test/e2e/filesystem-cache/warm-restart-task-stats.test.ts`, where the
timeout would otherwise cut off a Rust `on_exit` handler before it
writes its task statistics.

Both test cases previously guarded their request with a check for the
existence of the trace file, which the dev server creates on its own
once the first compile finishes. When that happened before the first
test body ran, neither case issued a request and the trace file
contained no `compile-path` or `render-path` span at all. The request
and the shutdown now happen once in `beforeAll`, and the fixed 500ms
sleep that followed the shutdown is replaced by a `retry` that waits for
the spans the assertions depend on.


---

<sub>Stack created with <a
href="https://github.com/github/gh-stack">GitHub Stacks CLI</a> • <a
href="https://gh.io/stacks-feedback">Give Feedback 💬</a></sub>
2026-07-25 07:21:02 +02:00
Hendrik Liebau 954eabec68 [refactor] Prepare dev validation for running on a worker thread (#96151)
This is a behavior-preserving refactor of the dev-mode Cache Components
validation, ahead of the change that moves it onto a worker thread.
In-process behavior is unchanged.

It narrows the context the validation functions require.
`runValidationInDev` and its helpers (`validateStaticShell`,
`warmupClientModulesForStagedValidation`, `validateStagedShell`,
`validateInstantConfigs`) now take a `ValidationRenderContext` (a `Pick`
of `AppRenderContext` plus the two `renderOpts` fields and the
debug-channel flag they actually read) instead of the full render
context, with `toValidationRenderContext` mapping the in-process and
build-time callers. The worker cannot reconstruct a full
`AppRenderContext` from a serialized snapshot, so narrowing the contract
to what validation genuinely consumes is what lets the same code run
there.

It also separates computing the validation errors from delivering them.
`runValidationInDev` now returns the errors instead of sending them to
the dev overlay inline, and the caller `runDevValidationInBackground`
delivers them via `logMessagesAndSendErrorsToBrowser`. The test-mode
lifecycle markers and delay move into `runWithDevValidationLogging`,
which brackets both the render and the delivery. When validation runs on
the worker, the worker computes the errors but the main thread still
delivers them (delivery needs the response object), so splitting the two
halves is a prerequisite.

Finally, it extracts the test-only validation lifecycle markers into a
shared `dev-validation-events` module. The `<VALIDATION_MESSAGE>`
wrapping and the event shape had been hand-rolled at each emission site
in `app-render.tsx` and duplicated again in the test harness; they now
live in one place as `formatValidationEvent` and an exported
`ValidationEvent` union that the in-process emitters call and the test
utilities import rather than redeclare. The worker emits the same
markers through the same helper once it is added, so a single definition
keeps every producer and the test parser in sync. Build validation's
marker `requestId`, previously the numeric `Date.now()`, is stringified
to fit the shared `string` type; it appears only in the logged marker
payload, which nothing matches on, so this too is unobservable.
2026-07-25 07:21:02 +02:00
Tim Neutkens 3a48dd6a6c Optimize app page entry analysis (#96058)
## Summary

- Extract the invariant app-page handler into a shared runtime module
while keeping each generated route entry as a small wrapper.
- Keep the `interopDefault` binding in the route wrapper because the
injected Turbopack metadata loader tree references it, and pass the same
binding into the shared runtime.
- Add a focused 14-route navigation benchmark with static metadata
coverage.

## Why

Turbopack gives every generated app-page entry a route-specific identity
because its loader tree, page name, pathname, and module hooks are
injected into the template. This caused the full app-page handler to be
analyzed independently for every route.

A clean native trace of the benchmark shows app-page analysis CPU
falling from 1,619.39 ms across 14 route copies to 153.92 ms total
across 14 small wrappers plus one shared runtime, a 90.5% reduction.
Concurrency-corrected analysis time falls from 181.63 ms to 97.87 ms.
The post-change fixture also includes a static icon to cover the
injected metadata loader binding.

## Verification

- `pnpm test-dev-turbo
test/e2e/app-dir/metadata-icons/metadata-icons.test.ts`
- `pnpm test-start-turbo
test/production/adapter-config/adapter-config-export.test.ts`
- `pnpm test-dev-turbo
test/development/app-dir/app-page-entry-prefetch-performance/app-page-entry-prefetch-performance.test.ts`
- `pnpm test-dev-webpack
test/development/app-dir/app-page-entry-prefetch-performance/app-page-entry-prefetch-performance.test.ts`
- Clean native reproduction: the metadata suite failed with
`ReferenceError: interopDefault is not defined` before the binding fix
and passed all 9 cases afterward.

<!-- NEXT_JS_LLM -->
2026-07-24 11:55:49 +02:00
Jiwon Choi 26824378a6 [ts-plugin] Preserve TypeScript quick info metadata (#95863)
### Why?

VS Code passes a maximum hover length and verbosity level to
TypeScript's `getQuickInfoAtPosition` API when a user expands a hover.
The Next.js TypeScript plugin intercepted that call and forwarded the
file name and position, dropping the remaining arguments. The plugin
also replaced TypeScript's `QuickInfo` object when it added config
documentation, which discarded native fields such as
`canIncreaseVerbosityLevel`. As a result, hovers for Next.js config
exports could not expand and lost native type information.

This supersedes #86290 with a narrower behavior change. The plugin
preserves native `QuickInfo` when it adds documentation to valid config
values and config identifiers. Invalid config values keep the existing
synthesized override, and hovers inside function initializers retain
TypeScript's existing behavior.

x-ref:
https://github.com/microsoft/vscode/issues/232685#issuecomment-3484167019

| Before | After |
|--------|-------|
| <img width="1656" height="390" alt="Config hover without expandable
metadata"
src="https://github.com/user-attachments/assets/ed39a851-7096-448b-bffe-55c5320c1713"
/> | <img width="1668" height="384" alt="Config hover with expandable
metadata"
src="https://github.com/user-attachments/assets/0a3a0cbc-5c75-4b6f-90fc-bbcef028d06b"
/> |

### How?

The plugin forwards all QuickInfo arguments to TypeScript and passes the
native result into the config hover rules. For valid config values and
identifiers, those rules copy the native `QuickInfo` fields and append
the Next.js documentation. They synthesize the existing fallback when
TypeScript returns no QuickInfo.

Focused tests cover argument forwarding, metadata preservation, missing
native QuickInfo, invalid config values, and function config boundaries.

### Test plan

Open a minimal Next.js app in Cursor with workspace TypeScript 6.0.2 and
the package built from this PR. Hover `export const metadata: Metadata`,
then select the expansion control. The hover expands from `Metadata` to
its full property type while the Next.js config documentation and link
remain visible.

<!-- NEXT_JS_LLM -->
2026-07-24 11:54:15 +02:00
dan b1713c8517 [RSC HMR] Fix a flurry of refetches when a editing component imported from many routes (#96102)
Fixes a flurry of requests when doing server HMR.

To reproduce the issue, create a server component imported from multiple
routes:

- Route `/a` imports that component
- Route `/b` imports that component
- Route `/c` imports that component
- (...and so on...)

Then edit that server component.

Expected: one request in terminal per open tab.
Actual: a flurry of requests.

There's two separate causes for the flurry of requests:

- The "server component updated" messages were emitted once per
already-compiled route, so a single tab would receive a flurry of
messages one right after another, and process them all.
- Also, the `debounce` for these messages is broken (it always fires
instead of delaying) due to a wrong condition inside it. So if we tried
coalescing messages by key, we'd still see multiple requests per tab.
- We should probably fix `debounce` but that's more risky because I
don't know if either existing caller could rely on the bug. Luckily, for
the fix below this doesn't matter.

Anyway, to get the desired behavior (one refresh per page), we switch to
expressing server component changes as a final message that gets sent at
the end of the update using a dirty flag. Then we don't need to worry
about how they get deduped or how many of them gets received.

There are probably other ways to factor this but this one seemed fine to
me.

## Before

A flurry of requests.


https://github.com/user-attachments/assets/337183e9-6fe2-4633-858d-6c1db6779618

## After

One request per open tab.


https://github.com/user-attachments/assets/3543619e-7ab3-4b74-bbdf-2151e16e2c2b

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 03:59:27 +01:00
Hendrik Liebau c3d5b92778 [test] Move the dev-only use cache test suite to test/development (#96023)
The `use-cache-dev` suite only covers dev-mode HMR behavior of `use
cache`, so it belongs under `test/development/app-dir` alongside the
other `cache-components-dev-*` suites rather than in `test/e2e`. Because
`test/development` runs only in dev mode and is never deployed, the
production-mode placeholder guard, the `skipDeployment` option, and the
`if (skipped)` early return are all dropped. The three test manifests
that referenced the old path are updated.


---

<sub>Stack created with <a
href="https://github.com/github/gh-stack">GitHub Stacks CLI</a> • <a
href="https://gh.io/stacks-feedback">Give Feedback 💬</a></sub>
2026-07-22 15:50:22 +02:00
dan 2549514abd [test] Add source-mapping coverage of React's fake stack frame scripts in use cache (#95945)
Adds tests verifying that`use cache` stacks get symbolicated.

We noticed insufficient coverage in
https://github.com/vercel/next.js/pull/95936, which should have failed
the tests (it broke symbolication) but tests passed. So this restores
the missing stack assertions. This also adds some new coverage that
specifically verifies that symbolicating via CDP also works.
2026-07-22 11:52:11 +01:00
Sam Poder 3ed60553da Revert "[turbopack] Don't SSR on pages only navigated to through a soft nav (#95539)" (#96028)
Causes issues with `use client` it seems.
2026-07-21 21:03:04 +00:00
Aurora Scharff 18d2e2da5f Insights: use a single Learn more link in console errors (#95967)
## Summary

The instant / blocking-prerender insight console errors printed a docs
URL under each fix option (two or three anchors per message). This
collapses them to a single `Learn more:` link at the end of each
message, while keeping the `[stream]`/`[cache]`/`[block]` fix-option
labels. It restores the single-link format these builders originally
shipped with.

**Before:**

```
Ways to fix this:
  - [stream] Provide a placeholder with `<Suspense fallback={...}>` around the data access
    https://nextjs.org/docs/messages/blocking-prerender-runtime#wrap-in-or-move-into-suspense
  - [block] Set `export const instant = false` to allow a blocking route
    https://nextjs.org/docs/messages/blocking-prerender-runtime#allow-blocking-route
```

**After:**

```
Ways to fix this:
  - [stream] Provide a placeholder with `<Suspense fallback={...}>` around the data access
  - [block] Set `export const instant = false` to allow a blocking route

Learn more: https://nextjs.org/docs/messages/blocking-prerender-runtime
```

Covers all 16 insight-kind errors, across `blocking-route-messages.ts`,
`sync-io-messages.ts`, `dynamic-rendering-utils.ts`, and
`instant-messages.ts`.

Because the dev overlay classified these errors by the `#`-anchored docs
URL, `getBlockingRouteErrorDetails` is updated to match the anchor-less
`Learn more:` URL. Console-message snapshots and the guidance-data
extraction test are updated to the new format. The dev-overlay fix-card
data (`instant-guidance-data.ts`) keeps its per-card links, since those
are the overlay UI rather than the console message.

## Verification

- `pnpm --filter=next types`
- Snapshots regenerated with `jest -u` for the affected suites

<!-- NEXT_JS_LLM -->
2026-07-21 12:53:17 +02:00
Tim Neutkens 81d0e8519d Fix instant validation blocking navigations (#95939)
### What?

- Run development instant validation only after the navigation response
has finished.
- Let a newer request supersede stale validation work and yield between
validation render attempts.
- Forward the cancellation signal into React validation prerenders so
asynchronous validation work can stop promptly.
- Add focused scheduler/response tests and assert that development
validation starts after the response finishes.

### Why?

Instant validation was detached from the response promise, but its
additional React renders still ran on the same Node.js event loop. A
subsequent navigation could therefore wait hundreds of milliseconds for
stale diagnostic work from the previous request.

Instant validation still runs when the server is idle. When another
navigation arrives, foreground request work now takes priority and the
obsolete validation is discarded.

### How?

Each Cache Components development request receives a validation
generation signal. Starting a newer request aborts the previous
generation. Validation waits for the Node response `finish` event,
yields through the event-loop poll phase between attempts, and combines
the generation signal with React's existing prerender abort signals.

Tests:

- `pnpm --filter=next build`
- `pnpm --filter=next types`
- `pnpm exec jest --runTestsByPath
packages/next/src/server/app-render/dev-validation-scheduler.test.ts
packages/next/src/server/app-render/wait-for-response.test.ts`
- `NEXT_SKIP_ISOLATE=1 NEXT_TEST_PREFER_OFFLINE=1 pnpm test-dev-turbo
test/e2e/app-dir/instant-validation/suspense-boundaries.test.ts -t
"valid - static prefetch - suspense around runtime and dynamic"`

<!-- NEXT_JS_LLM_PR -->
2026-07-21 09:53:43 +02:00
Sam Poder 20f5809320 [turbopack] Don't SSR on pages only navigated to through a soft nav (#95539)
This PR essentially implements:
https://github.com/vercel/next.js/pull/95539#discussion_r3555050929.
Will add more a description soon.
2026-07-17 13:10:54 -07:00
Andrew Clark 583ce21786 Back/forward set the Nav Inspector back to pending (#95865)
A back/forward (history traversal) navigation should not be captured by
the Nav Inspector, because it's not the same as a regular navigation: it
reads the entire navigation from cache, rather than fetching new data.
It might be the case that data in the bfcache is missing for some
reason, but in the normal case all the data will be available and the
navigation will be instant. So that's how we should model it in the Nav
Inspector.

In the future we may want to update the Nav Inspector UI to show that
the current screen was the result of a back/forward navigation, but for
now we will just reset the Nav Inspector back to the pending ("Waiting
for navigation...") state.

Includes the regression tests from #95793.

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
2026-07-17 11:44:05 -04:00
Andrew Clark 4395d7c807 Fix repeated navigations while the Instant Navigation lock is held (#95864)
With the Instant Navigation Testing lock held (the Navigation Inspector
paused), repeated client navigations between pages that share a layout
could sometimes get stuck without ever resolving.

The root cause turned out to be a subtle problem with the client
router's use of `useDeferredValue` to switch between the cached UI and
the final UI. The mechanism only works as we intend when mounting a new
part of the tree. When updating an existing part of the tree, like a
shared layout, React will skip the cached UI entirely.

We were already aware of this issue with the `useDeferredValue`
approach. The plan is to instead model cached navigation as an
optimistic update followed by a transition update. Before we can make
such a change, though, we need to make a change in React to allow
optimistic updates to suspend (currently, optimistic updates act like
sync updates, and can trigger existing Suspense boundaries to switch
back to their fallback state).

To workaround this issue when using the Navigation Inspector, we're
going to change how the inspector works when multiple navigations are
performed during the same "lock". Instead of keeping the entire sequence
blocked until the end, we will instead only block the most recent one.
This more closely matches what the inspector UI implies, anyway: it
doesn't show a stack of pending navigations, only the most recent one.

This doesn't absolve us from needing to switch to using an optimistic
update. That's needed regardless to address similar behavior in
production. But this sidesteps the issue for now, and we were planning
to make this change to the Navigation Inspector regardless.

Co-authored-by: Sam Selikoff <sam.selikoff@gmail.com>
2026-07-16 20:19:57 -04:00
Tim Neutkens 491f78099c Fix Request Insights span collection (#95818)
## Summary

Fix Request Insights collection so the dev server records all non-hidden
Next.js spans locally, including non-vanilla trace and wrapped spans,
while preserving the existing OpenTelemetry export allowlist unless
verbose tracing is enabled.

Keep reporting timelines correct by propagating request identity before
work async storage exists, honoring explicit span timestamps, and
anchoring request duration to the BaseServer request span. Standardize
span categories on `next.span_category`, retain presentation-only UI
filtering, and filter internal request identity headers at ingress.

## Verification

- `pnpm test-dev-turbo
test/development/app-dir/request-insights/request-insights.test.ts`

<!-- NEXT_JS_LLM -->
2026-07-16 14:24:43 +02:00
Andrew Imm ca414736ba telemetry: add agentName to anonymous metadata (#95586)
Add agentName to anonymous telemetry metadata

### What

Adds an agentName: string | null field to the anonymous telemetry
metadata block that accompanies every payload, alongside existing fields
like ciName, nextVersion, and isCI. The value is the detected AI coding
agent driving the process (e.g. claude, cursor, codex), or null when
none is detected.

### Why

We currently have no visibility into how much Next.js development
happens under AI coding agents. This field lets us understand that split
in the same anonymous way we already track other CLI usage.

### How

Agent detection via @vercel/detect-agent
- Vendored @vercel/detect-agent (from the Vercel CLI) into
`next/dist/compiled/@vercel/detect-agent` (new taskfile ncc task,
externals entry, and type declaration), matching how I *understand*
other third-party deps are bundled.
- The vendored detection method is async (it does a filesystem probe for
Devin in addition to env-var checks) and potentially slower than we'd
want for logging, so it's wrapped in a memoized helper:
`telemetry/agent-name.ts`. `getAgentName()` caches the promise from
`determineAgent()`: the first call runs detection once, concurrent
callers share that in-flight promise, and every later call resolves
against the already-settled result. The agent shouldn't change over the
process lifetime, so this is safe. Detection failures resolve to null so
telemetry never throws on this path.

Wiring into the payload
- `getAnonymousMeta()` is now async and includes `agentName: await
getAgentName()`. `storage.ts` awaits it on the send path (the record
method was thankfully already async).
- The NEXT_TELEMETRY_DEBUG=1 output now includes the meta block
alongside each event, so the full payload — including agentName — is
observable locally without sending anything. We didn't output this
before, so it was harder to debug these fields.

Consolidating agent detection
- There was a second, synchronous, hand-vendored copy of agent detection
(telemetry/detect-agent.ts) used only by the AGENTS.md/CLAUDE.md
auto-generation hook. It's been removed and that hook
(ensureAgentRulesForDev) now uses the same vendored `getAgentName()`.
Now we have a single source of truth and can easily get upstream changes
from the vercel CLI package. `ensureAgentRulesForDev` became async, its
only caller in start-server.ts was already in an async scope, so this is
easy.

### Testing

- NEXT_TELEMETRY_DEBUG=1 next build shows agentName in the payload meta
block next to ciName/nextVersion.
- Verified the memoized helper: agent present -> name; cleared env ->
null; repeated calls return the same promise instantly.
- agent-rules-auto-generate dev tests pass (agent-detected generates
files; cleared-env does not)
- next-server-nft trace snapshot updated: adds @vercel/detect-agent +
agent-name.js, removes detect-agent.js.
- ts, build, existing telemetry unit test, lint
2026-07-14 16:56:01 -07:00
Jimmy Lai 4a4590a37c request insights: add agent diagnosis access (4/5) (#93977)
## What

Adds `next experimental-diagnose` and an MCP `get_request_insights` tool
so agents and local users can inspect Request Insights without opening
the DevTools overlay.

Stack:

1. request insights: record local framework spans (1/5)
2. request insights: derive request history and fetch data (2/5)
3. request insights: expose dev snapshots to tools and HMR (3/5)
4. request insights: add agent diagnosis access (4/5) ← current
5. request insights: add DevTools request panel (5/5)

## Review Focus

- CLI error handling and raw JSON output.
- MCP gating when Request Insights is unavailable.
- Telemetry registration for the new MCP tool name.

## Proof In This PR

- `pnpm jest packages/next/src/telemetry/events/mcp-telemetry.test.ts
--runInBand`
- `pnpm --filter=next types`
- Stack-tip smoke verifies `next experimental-diagnose --url
http://localhost:3003 --json` against a real dev server.

## Deferred Coverage

- A more curated agent summary format is intentionally deferred until
the raw private snapshot contract settles.

<!-- NEXT_JS_LLM_PR -->

---------

Co-authored-by: Tim Neutkens <tim@timneutkens.nl>
2026-07-13 14:01:53 +02:00
Will Binns-Smith 8b7b6fea86 Turbopack tests: remove assertions that duplicate webpack results (#95688)
We forked test assertions in places in the early days of merging
Turbopack. Now that we more closely align, a bunch of these had the
exact same assertions. Fold them into one.
2026-07-10 12:11:26 -07:00
Janka Uryga 46681d90f0 [PPF] Sync IO is only allowed in the dynamic stage (#95384)
This PR makes Sync IO behavior more restrictive when
`partialPrefetching` is on (either globally or for a page).

- Previously, we allowed `await cookies(); Date.now()` in in segments
that won't ever be runtime prefetched. This was achieved by separating
segments into "early" and "late" stages and only erroring in the "early"
ones.
- After this PR, it will always be an error to do sync IO anywhere other
than after `io()`/`connection()`/uncached IO (i.e. outside the Dynamic
stage).

We're doing this mainly because with `partialPrefetching`, `cookies()`
resolves in App Shells, and sync IO causes a severe deopt in any
prerender. `allow-runtime` also opts a route into `partialPrefetching`,
which means that even the segments above the runtime prefetch boundary
will need an App Shell and can't tolerate Sync IO.

This PR removes all early/late stage separation, because we no longer
need to vary the Sync IO behavior on the segment. Instead, we now pick
whether a stageController should use
- `SyncIOMode.AllowedInRuntimeOrDynamic` (legacy)
- `SyncIOMode.AllowedInDynamic` (`partialPrefetching`)
- `SyncIOMode.Untracked` if it needs to opt out

I've removed the existing tests that asserted that sync IO is allowed in
segments above the `allow-runtime` boundary. Instead, we check that sync
io either errors (if partialPrefetching is on) or is allowed (if
partialPrefetching is off)

Closes NAR-855
2026-07-10 15:36:41 +00:00
Jude Gao 86ff18a4be Refresh outdated agent-rules blocks on next dev and codemod upgrade (#95470)
Once the managed agent-rules block is installed, `next dev` never
touches it again — `ensureAgentRulesForDev` gates on marker presence —
and `@next/codemod upgrade` doesn't refresh it either, so changes to the
block's content (like the wording fix in #95467) never reach existing
projects.

`next dev` now gates on `hasCurrentAgentRules` and re-upserts a block
whose content differs from canonical, targeting whichever file already
hosts it so an upgrade rewrites the block in place instead of
duplicating it into a second file; legacy `NEXT-AGENTS-MD` blocks
migrate the same way, and CRLF files keep their line endings.
`@next/codemod upgrade` refreshes the block after the version bump by
delegating to the installed package's own generator
(`next/dist/server/lib/generate-agent-files`), so the codemod never
carries a copy of the wording and no-ops below 16.3 or when the project
never adopted the block. A malformed block (start marker without end)
converges to a single canonical block within two runs rather than
growing unboundedly.

The e2e test now covers the in-place upgrade (surrounding content
preserved, exactly one block, no `CLAUDE.md` created when `AGENTS.md`
hosts it) and idempotence across dev-server restarts, using the real
generator's output rather than hardcoded wording.
2026-07-08 22:58:49 -04:00
Benjamin Woodruff a87692e17c [ci] Split up large rsc-build-errors development test (#95624)
See https://github.com/vercel/next.js/pull/95553 for an explanation of
why we want to split these up. TL;DR it makes CI faster and saves us
some money.

In CI we have to run tests at the suite/file granularity, so if a test
suite is too long it limits parallelism and if it has to be retried, we
have to retry the full suite.
2026-07-08 19:56:27 -07:00
Benjamin Woodruff 337a40c672 [ci] Pin typescript version in tests (#95619)
Typescript 7 came out, which is breaking a lot of our e2e tests.
2026-07-08 14:12:48 -07:00
Sam Poder bb9af37519 [turbopack] Rename rscEndpoint to rscHmrEndpoint (#95538)
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`.
2026-07-07 18:19:40 -07:00
Benjamin Woodruff c3dd28fd6a [ci] Split up large cache-components-dev-warmup test suite (#95553)
This splits two of our slowest test suites into smaller pieces that can
be run in parallel and retried in isolation.

## Context

Jest treats test suites as the smallest test execution unit.

Slow test suites can lead to situations where a test shard is using very
little CPU waiting for one last test suite to finish, and when tests
fail and have to be retried, we have to retry the entire suite.

Here's a particularly bad example:

<img width="3116" height="2634" alt="Screenshot 2026-06-08 at 20-00-05
CI Telemetry — turbopack production tests (1)"
src="https://github.com/user-attachments/assets/b5534200-4b77-40d5-a219-4b6fc552ed77"
/>

I instructed Claude to fetch test timing information from GitHub Actions
Artifact, and to report the slowest test suites.

> How timings work today
>
> - The `fetch-test-timings` job runs `node run-tests.js --timings
--write-timings`, which pulls per-file durations from a Vercel KV store
and uploads them as the test-timings artifact (test-timings.json, 5-day
retention).
> - Each sharded job downloads that artifact and run-tests.js -g N/M
greedily bin-packs whole test files into groups by predicted duration
(run-tests.js:528-549).
> - I pulled the artifact from the July 7 canary run (28843797348): 1684
test files, ~61,500s total, median 20s, but max 1023s.
>
> Since the file is the unit of scheduling, a shard can never finish
faster than its largest file. The job durations from that run confirm
the imbalance this causes: test dev 4/10 took 28 min while siblings took
10–17; test cache components dev 6/6 took 13 min vs 5–6 for siblings;
test turbopack production 5/7 took 20 min vs 10–11.

> The slowest suites

```
┌────────────────────┬────────────────────────────────────────────────────────────────────────────┐
│        Time        │                                   Suite                                    │
├────────────────────┼────────────────────────────────────────────────────────────────────────────┤
│ 1023s + 933s       │ test/development/app-dir/cache-components-dev-warmup/ (both variants)      │
├────────────────────┼────────────────────────────────────────────────────────────────────────────┤
│ 942s               │ test/production/create-next-app/templates/matrix.test.ts                   │
├────────────────────┼────────────────────────────────────────────────────────────────────────────┤
│ 788s + 432s + 232s │ test/e2e/app-dir/cache-components-errors/cache-components-errors.*.test.ts │
├────────────────────┼────────────────────────────────────────────────────────────────────────────┤
│ 779s               │ test/development/acceptance-app/rsc-build-errors.test.ts                   │
├────────────────────┼────────────────────────────────────────────────────────────────────────────┤
│ 470s               │ test/e2e/opentelemetry/instrumentation/opentelemetry.test.ts               │
├────────────────────┼────────────────────────────────────────────────────────────────────────────┤
│ 457s               │ test/e2e/app-dir/actions/app-action-node-middleware.test.ts                │
├────────────────────┼────────────────────────────────────────────────────────────────────────────┤
│ 451s               │ test/e2e/app-dir/next-after-app/index.test.ts                              │
├────────────────────┼────────────────────────────────────────────────────────────────────────────┤
│ 447s               │ test/e2e/app-dir/css-order/css-order.test.ts                               │
├────────────────────┼────────────────────────────────────────────────────────────────────────────┤
│ 426s               │ test/e2e/telemetry/config.test.ts                                          │
├────────────────────┼────────────────────────────────────────────────────────────────────────────┤
│ 402s               │ test/production/debug-build-path/debug-build-paths.test.ts                 │
└────────────────────┴────────────────────────────────────────────────────────────────────────────┘
```

> Reading these files, almost every one is slow for the same structural
reason: a `describe.each(...)` over fixtures/modes/runtimes where each
iteration boots its own `nextTestSetup` (dev server or full production
build), run serially within one Jest worker:

> - **cache-components-dev-warmup:** `describe.each` over 2 fixture
dirs, and it restarts the dev server (stop/clean/start) before every one
of ~17 tests. It's slow by design (cache-warmup isolation), so per-test
cost can't shrink — but the file can be split.
> - **create-next-app matrix:** `describe.each(app|pages)` × `it.each`
over a ~16–48-combination flag matrix, each doing a full CNA scaffold (+
dev-server cycle for pages), fully serial.
> - cache-components-errors.test.ts: 7490 lines, 129 tests, with
per-describe `next build --experimental-build-mode compile` and per-path
generate builds in prod mode.
> - **css-order:** three sibling `describe.each` blocks over 4–6
chunking modes, each mode booting its own server, ~3 blocks × modes ×
page-pair orderings.
> - **next-after-app, opentelemetry:** `describe.each` over
runtimes/configs, again one server per iteration.
2026-07-07 14:51:23 -07:00
Hendrik Liebau 60ddf0966a Cancel a superseded Server Components HMR refresh's server-side work (#95486)
When rapid edits overlap Server Components HMR refreshes in `next dev`,
only the newest refresh can commit. The client already aborts a
superseded refresh's fetch, which closes its response. We use that
response-close to stop the server work the superseded refresh started,
so the dev server no longer runs a render (and, under Cache Components,
a validation) whose result is discarded.

When the `serverComponentsHmrCancellation` flag is enabled and the
request is an HMR refresh over a Node response, we derive an abort
signal from `signalFromNodeResponse(ctx.res.originalResponse)` and
thread it into the Flight render. This covers both dev RSC render paths:
the Cache Components staged render in
`generateDynamicFlightRenderResultWithStagesInDev` and the
non-Cache-Components render in `generateDynamicFlightRenderResult`.

The render is aborted through `renderToNodeFlightStream`, which calls
`abort()` on the pipeable that `renderToPipeableStream` returns when the
signal fires. React's Node Flight API has no `signal` option, unlike the
Web `renderToReadableStream`, aborting the returned pipeable is the
intended way to stop the render. This also removes the incorrect
`signal` field from the `renderToPipeableStream` type declaration and
derives `FlightRenderOptions` from `renderToReadableStream`'s options,
so the render wrappers are no longer typed as `any`.

Under Cache Components the superseded refresh additionally skips its
detached validation, which only runs on the staged render, so it never
validates a discarded tree, and any error thrown while its background
renders tear down is swallowed. The aborted Flight renders surface as
abort errors that `createReactServerErrorHandler` already ignores, so
nothing reaches the error overlay or the CLI.

The `hmr-rsc-cancellation` suite exercises both render paths, with Cache
Components controlled by the `__NEXT_CACHE_COMPONENTS` test shard rather
than the fixture config. A child component logs when React renders it,
and the test asserts the superseded refresh never logs, since its render
is aborted before reaching the child, while the committed refresh does.
The detached-validation assertions run only when Cache Components is
enabled.
2026-07-07 21:53:00 +02:00
Benjamin Woodruff 727a3ffa86 [ci] Skip apt-installed dependencies in playwright install step (#95535)
The hypothesis is that this is just installing some font stuff that
isn't actually needed for Chromium to work:

```
The following additional packages will be installed:
  libnss3-tools xfonts-encodings xfonts-utils
Recommended packages:
  fonts-ipafont-mincho fonts-liberation-sans-narrow fonts-tlwg-loma
The following NEW packages will be installed:
  fonts-freefont-ttf fonts-ipafont-gothic fonts-liberation fonts-tlwg-loma-otf
  fonts-unifont fonts-wqy-zenhei xfonts-cyrillic xfonts-encodings
  xfonts-scalable xfonts-utils
The following packages will be upgraded:
  libnss3 libnss3-tools
2 upgraded, 10 newly installed, 0 to remove and 35 not upgraded.
```

`apt` is very slow, so skipping this step should significantly reduce
setup time per test shard.

It seems like this isn't always true for other browsers, so only skip
this for Chromium (that's fine, that's what most of our jobs use
anyways).

On 16 core arm runners, "Install Playwright Browsers" typically took
18-24 seconds. Now it takes 5-6s.

Let's estimate that saves about 15s per Linux job that installs
Chromium. Claude estimates that there are 73 chromium-only Linux jobs on
this PR:

<img width="891" height="148" alt="Screenshot 2026-07-06 at 10 30 16 PM"
src="https://github.com/user-attachments/assets/725893fa-9f6f-4806-a3b4-9858ab0ebe66"
/>

So that's 15*73/60=18.25 minutes saved per PR push (the average PR gets
many pushes). At the public (before any volume discounts) price of
$0.026/minute for the 16 core arm runners, that comes out to $0.4745 per
PR push saved.

This workflow has had 3,894 runs in the last 30 days, so that could be
$1,847/mo saved, but many of those workflow runs were canceled, and some
likely failed before getting to these jobs, so the actual savings are
probably somewhere around half of that -- scroll through
https://github.com/vercel/next.js/actions/workflows/.github/workflows/build_and_test.yml
and see that a little less than half of the workflow runs end up
canceled.
2026-07-07 09:48:46 +02:00
Hendrik Liebau 5f57774334 Abort superseded Server Components HMR requests on the client (#95463)
In `next dev`, rapid edits can start overlapping Server Components HMR
refreshes, but only the newest can commit. When a newer refresh
supersedes an older one, we abort the older request on the client so it
stops transferring and decoding RSC data, and we keep the browser from
reissuing the superseding request as a duplicate. Everything here is
gated on the `serverComponentsHmrCancellation` flag.

On the client, the router's `hmrRefresh` method tracks the newest
refresh generation in an `AbortController` and aborts the previous one
before scheduling the new one, threading the abort signal through the
HMR action down to the `fetch` call. An aborted request becomes a
`NavigationTaskExitStatus.Canceled` rather than a failure, so
`finishNavigationTask` leaves its cache nodes for the newer navigation
to fulfill and does not retry or fall back to an MPA navigation.
`hmrRefreshReducer` also bails out when its generation was aborted
before it ran, which happens when an HMR action waits behind a Server
Action in the router queue.

The subtle part is how an aborted request's partially received response
is handled. A superseded refresh can already have committed part of its
tree with a Suspense boundary still streaming. Rather than letting the
aborted fetch reject the still-pending rows, which a committed boundary
would throw on, we decode the response through a wrapper stream that we
close, so React marks the unresolved rows as halted: they suspend during
render instead of rejecting, and the superseded boundary keeps its
fallback until the newer response commits. Closing the stream also
avoids the unclosed-stream memory leak that #89610 fixed for prefetch
streams.

Aborting the superseded fetch exposed a Chromium-only side effect. In
development, App Router responses are served with `Cache-Control:
no-cache, must-revalidate` so the browser can restore them on back and
forward navigation, which keeps each response stored and keyed by URL.
Successive refreshes of the same page share one cache entry, and
aborting the superseded refresh mid-write leaves that entry
half-written; Chromium discards it and reissues another superseding
refresh on a second connection. The dev server then renders it twice and
emits its debug channel twice under the same request id, producing
`Cannot write to a CLOSED writable stream` errors on the client. Firefox
and Safari do not reissue. To prevent this we serve HMR refresh
responses, identified by the `next-hmr-refresh` request header, with
`Cache-Control: no-store`, so no shared entry exists for an aborted
refresh to leave half-written. All other dev responses keep `no-cache`,
so their restore behavior is unchanged.

A development test suite covers a superseded request being aborted while
the newest commits without a hard reload, a partially committed render
whose Suspense boundary is left streaming not surfacing an error when
superseded, and disabling the flag preserving the previous behavior. The
supersession tests also assert no browser console errors, which guards
against the duplicate debug channel regression.

Cancelling the superseded request's server-side render and validation,
so the dev server also stops the discarded work, is left to a follow-up.
2026-07-06 21:15:39 +02:00
Sebastian "Sebbie" Silbermann f656c597c6 [fragment-scroll] Enable new scroll handler by default (#95378) 2026-07-06 18:39:07 +00:00
Hendrik Liebau 4d9e955c01 [test] Unflake cache-components-dev-streaming test suite (#95466)
[Flakiness
metrics](https://app.datadoghq.com/ci/test/runs?query=test_level%3Atest%20%40git.repository.id%3A%22github.com%2Fvercel%2Fnext.js%22%20%40test.type%3A%22nextjs%22%20%40test.status%3A%22fail%22%20%40test.suite%3Acache-components-dev-streaming&agg_m=count&agg_m_source=base&agg_t=count&fromUser=false&index=citest&start=1782496431626&end=1783101231626&paused=false)

<img width="425" height="170" alt="Screenshot 2026-07-03 at 19 55 48"
src="https://github.com/user-attachments/assets/7751d204-1ceb-4cc0-b90e-8f8f70adb803"
/>

The streaming assertions in this suite used `elementByCssInstant`, which
runs `page.waitForSelector` with a 10ms timeout. That budget is below
the floor of a Playwright selector query under CI load: the call is a
cross-process round-trip to the browser, and in a streaming render the
page's main thread is busy processing chunks, so the injected visibility
poll frequently cannot complete within 10ms even when the element is
already present and visible. Playwright then rejects with a timeout, and
the surrounding `retry()` only hides this behind a coarse 500ms poll,
leaving the tests racy. We have observed the instant selector throwing
immediately after a prior assertion had already confirmed the same
element was present.

This change replaces those `elementByCssInstant` and `retry` blocks with
`elementByCss(selector, { waitUntil: false })`. That gives the selector
query Playwright's default 5s budget and its efficient built-in waiting,
while still skipping the page `load` event, which (with `waitUntil:
'commit'`) only fires once the slowest cache has filled. The `retry()`
wrappers become redundant because every target here is a distinct id
from its fallback (for example `#dynamic` versus `#dynamic-fallback`),
and the Suspense boundary reveals the content subtree in a single
commit, so the moment the element exists its text is already final.

The `#cached` assertion keeps an explicit `timeout: 10000` because its
cache fill takes about 5s, which would race the default 5s selector
timeout. The `retry()` around the bare `<p>` in the `use-cache` test is
left in place: there the fallback and the content are both an id-less
`<p>`, so the same element's text changes from `Loading...` to a date,
and only a retry can observe that transition.
2026-07-06 12:27:49 +02:00
Hendrik Liebau dd8b154d9f Ignore-list internal frames whose source maps chain to original sources (#95448)
Next's dev error output (terminal logs and the browser overlay)
ignore-lists framework-internal stack frames. The forked
`EvalSourceMapDevToolPlugin` recomputes this ignore-list per module
because the vendored `webpack-sources` has no runtime `ignoreList`
support, so it drops the field when it combines a module's input source
map with the generated mappings. `shouldIgnorePath` matches on the
emitted source path, which works while a source resolves back to its
module. But a Next-internal module that ships an input source map (its
`.js.map`, which taskfile-swc marks entirely ignore-listed) resolves its
sources to the original files such as `src/server/web/adapter.ts`, which
no longer contain the `node_modules`/`next/dist` marker and no longer
resolve back to a module. Those frames then escape ignore-listing and
leak into both the terminal output and the overlay.

For such unresolved sources this falls back to the module's own resource
path, so a frame whose module lives in `node_modules`/`next/dist` stays
ignore-listed even when its source map points at the original source.
The fallback applies only to a `NormalModule`, whose sources all belong
to that one module; a `ConcatenatedModule` merges several modules'
sources under one map and is left to the path check, which the dev
`eval-source-map` devtool never produces anyway.
2026-07-03 14:27:48 +02:00
Hendrik Liebau f83bc252fc Type resolved cacheLife profiles, dropping runtime asserts (#95428)
Runtime `"use cache"` code validated the default cache-life profile on
every invocation because `cacheLifeProfiles` was typed as optional with
partial profiles: `assertDefaultCacheLife` on the generate and RDC-read
paths, a per-`cacheLife()` presence `InvariantError`, and optional
chaining on reads.

Config normalization already guarantees this once:
`assignDefaultsAndValidate` builds the config from `{...defaultConfig,
...config}`, so `cacheLife` is always present, and it backfills the
`default` profile's `stale`, `revalidate`, and `expire`. This moves that
guarantee into the type. A new `ResolvedCacheLifeProfiles` (in
`config-shared`) types the `default` profile as `Required<CacheLife>`
and overrides `NextConfigComplete.cacheLife`, and the type is threaded,
non-optional, through the render options, work store, and the build,
export, and dev workers. Runtime code now reads
`cacheLifeProfiles.default` directly, and the asserts, the presence
guard, and the optional chaining are gone.

The proxy (middleware) work store is the one construction site without a
resolved profile: the proxy does not support `"use cache"`, so it never
reads `cacheLife`. It is given a sentinel whose `default` getter throws
if ever read, matching the "never read" sentinels already used for its
other unused render options.
2026-07-03 14:27:47 +02:00
Hendrik Liebau 2850659b74 Cache short-expire 'use cache' values across dev reloads (#95362)
Development has recently gained several mechanisms that make `'use
cache'` reloads fast under Cache Components: `'use cache: private'`
entries are persisted in a dedicated in-memory handler,
`cacheMaxMemorySize: 0` uses a real in-memory handler instead of the
no-op stub, and custom handlers are fronted by a fast built-in handler
through the tiered handler. One case was still missing. A value that
opts into a dynamic, client-only life with an explicit short `expire`
(for example `cacheLife({ expire: 0 })`, or the built-in `'seconds'`
profile) was treated as a miss on every reload, for both the built-in
default handler and custom handlers, so reloads re-ran the cache
function and streamed slowly.

The reason is that `expire` is the value's expiration bound, the longest
it may still be served before it has to be treated as expired. That is
its purpose in both dev and production; what differs is which threshold
the built-in in-memory handler enforces. In `next dev` it serves stale
entries up to `expire` to keep reloads fast, whereas in production it
drops them earlier, once past `revalidate`. An `expire` of zero
therefore leaves the dev handler no window in which a reload can be
served from the cache, and the wrapper's serve-vs-regenerate check,
which also keys on `expire`, regenerates instead.

This change extends the same dev-only treatment to those values without
altering their resolved cache life. The built-in default handler now
retains an entry for at least `MIN_PRERENDERABLE_EXPIRE` in dev, a
minimum the custom front handler inherits by being a built-in default
handler, and the wrapper applies the same minimum when deciding whether
to serve or regenerate. That affects the retain and serve decisions
only; the entry keeps its real `expire`, so the staged dev render still
resolves it at the appropriate stage rather than in the shell stage. A
short-`expire` entry is also re-warmed in the background on every
dynamic request render, so a reload serves the previously cached value
immediately and the freshly recomputed one appears on the next reload.
This is the same stale-while-revalidate trade-off already accepted for
the private-cache and `cacheMaxMemorySize: 0` dev optimizations, which
likewise favor a fast reload over serving a value these configurations
would not otherwise cache at all. For custom handlers the re-warm
re-executes the function and writes through to the backing.

Unlike the private-cache case, we deliberately do not force a dynamic
cache life here, because forcing `revalidate: 0` would leak into the
cache life propagated to an enclosing `'use cache'` and trip the
nested-dynamic error with the wrong message. And unlike the size-0 case,
keeping the resolved life alone is not enough, because a short `expire`
is exactly what makes the dev handler drop the entry, which is why the
minimum retention is needed. Because the dev front handler now enforces
that minimum, the tiered handler can no longer evict a stale front entry
by writing `expire: 0` (the minimum would keep it alive), so
`toExpiredEntry` now writes a negative `expire`, which the default
handler recognizes as an eviction sentinel and reports as missing
regardless of the retention minimum. This mirrors the existing
`revalidate = -1` convention, though a negative `expire` means the entry
is dropped rather than served-but-revalidated.

Everything is gated on `process.env.__NEXT_DEV_SERVER`, so production
behaves exactly as before: short-`expire` values keep their real cache
life, and configured handlers are used directly. New development tests
cover the built-in and custom-handler cases, asserting that a cache-miss
navigation shows the Suspense fallback while a cache-hit one does not,
and that a reload serves the cached value yet converges to a fresh one.


---

<sub>Stack created with <a
href="https://github.com/github/gh-stack">GitHub Stacks CLI</a> • <a
href="https://gh.io/stacks-feedback">Give Feedback 💬</a></sub>
2026-07-02 11:26:08 +02:00