mirror of
https://github.com/vercel/next.js.git
synced 2026-09-20 02:25:18 +08:00
v16.3.1-canary.7
1257 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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). |
||
|
|
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. |
||
|
|
2c04735944 |
[fragment-scroll] Remove config.experimental.appNewScrollHandler (#95602)
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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 --> |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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 --> |
||
|
|
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 --> |
||
|
|
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> |
||
|
|
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. |
||
|
|
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 --> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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 |
||
|
|
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> |
||
|
|
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. |
||
|
|
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 --> |
||
|
|
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 --> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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. |
||
|
|
3ed60553da |
Revert "[turbopack] Don't SSR on pages only navigated to through a soft nav (#95539)" (#96028)
Causes issues with `use client` it seems. |
||
|
|
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 -->
|
||
|
|
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 --> |
||
|
|
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. |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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 --> |
||
|
|
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 |
||
|
|
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> |
||
|
|
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. |
||
|
|
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 |
||
|
|
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. |
||
|
|
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. |
||
|
|
337a40c672 |
[ci] Pin typescript version in tests (#95619)
Typescript 7 came out, which is breaking a lot of our e2e tests. |
||
|
|
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`. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
f656c597c6 | [fragment-scroll] Enable new scroll handler by default (#95378) | ||
|
|
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. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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>
|