Commit Graph

15 Commits

Author SHA1 Message Date
Jiwon Choi d7aa66c345 Remove generated error codes (#97687)
### Why?

Should come up with better solution that does not block PRs with git
conflict

x-ref:
https://vercel.slack.com/archives/C02CDC2ALJH/p1785263902728189?thread_ts=1785263687.502649&cid=C02CDC2ALJH

### How?

- Delete `errors.json`, the error-code SWC plugin, generated WASM, merge
driver, and validation/build tooling.
- Stop attaching error codes to server-rendering digests, redboxes, and
telemetry; native `Error.code` and `Error.name` remain available where
applicable.
- Remove the development-overlay error feedback UI, middleware, and
telemetry event that depended on stable codes.
- Update fixtures, snapshots, and guidance for code-free errors and
numeric-only digests.

<!-- NEXT_JS_LLM -->
2026-08-21 22:45:12 +02:00
Hendrik Liebau 529ddc3c8d [test] Unflake two cache-components-dev-streaming assertions (#97246)
The streaming assertion in `should stream suspense boundaries while
filling caches in the background` polled a bare `<p>` through `retry()`
with its 3000ms default. Because `retry` gives up as soon as `waited +
interval > duration`, the effective budget is about 2.6s, and the
fixture's cache fill alone takes 2000ms, so the assertion had roughly
600ms of headroom. That budget also had to absorb one browser round trip
per attempt, because a selector that matches the fallback as well as the
content cannot wait for anything: `waitForSelector` returns the fallback
immediately, so the waiting had to happen in the test process at a 500ms
granularity.

This change gives the two paragraphs the ids `#cached-fallback` and
`#cached`, in line with every other route in this fixture, which is what
lets the wait move into the browser. Playwright now resolves the moment
the content is revealed, in a single round trip, and #95466 had already
moved the rest of the suite to that pattern. The two assertions that
check what the shell itself delivers still need the original "whichever
element arrives first" probe, and they now express it as the selector
list `#cached, #cached-fallback` instead of relying on `p` to match
both.

The budget for the reveal is 10s rather than the 5s that `elementByCss`
narrows the harness default to, because the wait has to cover more than
the fill. In the linked failure the content bytes had arrived one second
after the shell committed, and the reveal was still at least 2.4s away,
since the browser was busy evaluating the dev bundle. A shorter fill
would not help with that, as it does not shorten the part of the wait
that CI actually spends.

The convergence assertion in `serves a short-expire cache warm on reload
and converges to a fresh value` ran out of the same budget for a
different reason: every attempt performed a full dev page reload on top
of the 1.5s regeneration, and measured runs needed 1.8s to 3.4s. It now
reads the value over HTTP with `next.render$`, the way #97187 does,
which observes the same server-side cache state without downloading and
evaluating the dev bundle. Under the same contention those reads
converge in 1.1s to 1.9s, and they stay there when the load is
quadrupled, because only the 1.5s regeneration gates the loop. The
default budget therefore covers it, and the test drops from 15.5s to
3.6s.

Both fixes were verified by raising the fill to 3.2s to emulate the
CI-side delay: the previous assertions then fail deterministically with
the CI signature, and the new ones pass.

[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%22cache-components-dev-streaming%20should%20stream%20suspense%20boundaries%20while%20filling%20caches%20in%20the%20background%22%20%40test.type%3A%22nextjs%22%20%40test.status%3A%22fail%22&agg_m=count&agg_m_source=base&agg_t=count&citest_explorer_sort=timestamp%2Casc&cols=%40test.status%2Ctimestamp%2C%40test.suite%2C%40test.name%2C%40duration%2C%40test.service%2C%40git.branch&currentTab=overview&eventStack=&fromUser=true&index=citest&start=1783948256248&end=1786540256248&paused=false)
2026-08-12 21:53:40 +02: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
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
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 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
Aurora Scharff 41114a31b8 Remove 'silence this warning' from instant validation fix output (#95187)
## What

Removes the `silence this warning` phrasing from the structured fix
lines in instant validation output:

- `- [block] Set \`export const instant = false\` to ~~silence this
warning and~~ allow a blocking route`
- `- [ignore] Set \`export const instant = false\` to ~~silence this
warning and~~ opt the route out of instant-navigation validation`
- `- [ignore] Set \`export const instant = false\` to ~~silence this
warning and~~ opt the dropped segment out of instant-navigation
validation`

## Why

The line called itself a warning while being logged via `Error:`. This
PR originally renamed it to `silence this error`, but that has the
mirror problem: at validation level `warning` or `manual-warning` (or
when the check only surfaces in dev) nothing blocks, so "error"
over-claims.

The verb is also wrong either way. Setting `instant = false` doesn't
silence a problem that still exists. It declares that blocking is
acceptable for the route, so validation stops treating it as one.
Removing the clause leaves wording that is correct at every validation
level and matches the dev overlay cards ("Allow blocking route",
"Disable validation on this route") and the docs sections the lines link
to.

## How

- `blocking-route-messages.ts`, `dynamic-rendering-utils.ts`: drop
`silence this warning and` from the `[block]` lines
- `instant-messages.ts`: drop it from the `[ignore]` lines (unrendered
segment, link prefetch)
- `errors.json`: regenerated, append-only (codes 1394-1405)
- Storybook fixture and 15 test files updated to the new strings

<!-- NEXT_JS_LLM_PR -->
2026-07-01 20:42:56 -04:00
Janka Uryga 3767dfae1f test: fork select tests on partialPrefetching (#95279)
Stopgap until we make this a proper part of the CI matrix
2026-06-29 22:43:34 +00:00
Janka Uryga 8c6cb0e080 [PP] Reveal after ShellRuntime when simulating a Shell Prefetch in dev (#95149)
When `partialPrefetching` is on, the shell that we usually care about
displaying is the App Shell, which is represented by the ShellRuntime
stage. We should only show the cold cache indicator for caches that
happen up until then, and release the client-side promise appropriately.
This PR extends the existing `revealAfterStage` mechanics to account for
this.

> Note that this is incomplete: if we're navigating via `<Link
prefetch={true}>` (or have `partialPrefetching: "unstable_eager"), we
might want to use the Runtime stage instead. I'm leaving those for a
follow up -- representing the common case (a shell prefetch) seems like
the most useful thing.

I've re-worked the `revealAfterStage/holdStreamUntilRevealed` setup into
an object (`DevNavigationKind`) that represents the navigation that
we're trying to simulate -- either an initial load or a client nav. This
gets rid of the invalid `holdStreamUntilRevealed = true` +
`revealAfterStage = RenderStage.Runtime/ShellRuntime` combination and
lets us bring the logic closer to where the stream blocking tricks
actually happen.

I've also done some drive-by refactoring of `streamStagedRenderInDev` to
dedupe some repetitive code, because every task was basically doing the
same thing. I've also moved all `revealAfter.resolve()` calls into
separate tasks -- we were inconsistent about this, and the `Static`
`revealAfter.resolve()` was done together with the next stage, but the
`RenderStage.Runtime` was done in a separate task.

---

Strangely, despite the changes working as expected for the Cold Cache
indicator, I can't actually get link data (`await searchParams`) to
trigger a fallback when navigating, so there's a failing test for that
in `cache-components-dev-streaming.test.ts`. I'm not sure why what's
causing this, but I'm leaving that investigation for a follow-up as
well.
2026-06-25 14:46:17 +00:00
Aurora Scharff f965c00411 Insights: drop irrelevant fix cards from instant errors (#94926)
### Why?

Two Insight fix cards were misleading:

1. **`generateStaticParams` showed up on every runtime/client-hook
insight.** One error covers `cookies()` / `headers()` / `params` /
`searchParams`. GSP only applies to `params`; for the others it's noise.
Even for `params` it nudges devs to make the route static instead of
fixing the immediate error.
2. **`"use cache"` showed up on `connection()` triggers.** Caching
`connection()` is contradictory.

Both manifest on initial load and in-navigation (the fix-card sets are
shared).

### What?

1. **Drop the GSP card** from runtime + client-hook sets. Affects
[01-cookies-body](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/01-cookies-body),
[03-params-body](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/03-params-body),
[90-client-use-params](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/90-client-use-params),
[41-subnav-cookies](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/41-subnav-cookies),
[42-subnav-fetch](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/42-subnav-fetch).
2. **Filter `"use cache"`** when the cause is `connection()`. Affects
[05-connection-body](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/05-connection-body),
[08-connection-body-dynamic](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/08-connection-body-dynamic),
[31-connection-in-metadata](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/31-connection-in-metadata),
[33-connection-in-viewport](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/33-connection-in-viewport).
[06-uncached-fetch-body](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/06-uncached-fetch-body)
keeps the Cache card.

### How?

- `getCards()` filters the cache card when `cause === 'connection'`.
- New `deriveCauseFromCodeFrame()` helper detects `connection(` on the
highlighted code-frame line.
- `ParamClientHookDynamicError` collapsed into `ClientHookDynamicError`.
- CLI/build messages: dropped the GSP bullet; added `(does not apply to
\`connection()\`)` on the cache bullet.
- Docs: removed `For known params, prerender` sections; added a
connection caveat on dynamic/metadata-dynamic/viewport-dynamic pages.

<!-- NEXT_JS_LLM_PR -->
2026-06-23 19:27:11 +02:00
Hendrik Liebau 96e9a8e246 Make cacheMaxMemorySize: 0 and custom cache handlers fast in dev (#94784)
When Cache Components is enabled, `next dev` treats a `'use cache'`
value as a miss and renders as if the cache were empty whenever the read
does not resolve right away. Two development configurations triggered
that even for values that were already cached, so warm reloads streamed
slowly instead of serving the cached value: `cacheMaxMemorySize: 0`
replaced the built-in default handler with a no-op stub, so nothing was
cached at all, and custom cache handlers with a slow or remote `get` did
not return in time.

For the size-0 case, development now uses a real in-memory handler
instead of the no-op stub, and the `'use cache'` wrapper forces a
dynamic cache life (`revalidate: 0`, a 5-minute `expire`) for it, the
same treatment private caches already receive, so every read serves the
stale entry and re-warms a fresh one in the background. This also fixes
the dev private handler, which was sized from `cacheMaxMemorySize` and
so degraded to the no-op stub whenever `cacheMaxMemorySize: 0` was set.

Custom cache handlers keep their configured cache life, since their
backing owns it. Instead we put a fast built-in in-memory front handler
in front of the configured one through the new `TieredCacheHandler`,
which serves warm reads from the front, writes through to both tiers,
and reconciles the front against the backing in the background, evicting
the front entry when the backing no longer has it (the handler interface
has no per-key delete, so it overwrites the entry with an
already-expired copy). These dev-only handlers are kept out of the
registered handler set and merged in only where tag operations iterate,
so `revalidateTag` still reaches them.

Everything is gated on `process.env.__NEXT_DEV_SERVER`, so production is
unchanged: `cacheMaxMemorySize: 0` still caches nothing, private entries
are still never persisted, and configured handlers are used directly.
New development test suites cover the size-0 and custom-handler
behavior.
2026-06-16 18:06:28 +02:00
Hendrik Liebau 66496a4c56 Avoid a premature Suspense fallback flash in the streaming dev render (#94783)
The Cache Components streaming dev render streams the response while
filling caches in the background, but when those caches are already warm
it should behave like a production render, delivering the cached content
as part of the shell rather than behind a Suspense fallback. The render
previously held the stream back until the shell stage had been buffered
before releasing it. That was enough for the SSR render, whose Fizz pass
consumes the buffered shell server-side, but not for the browser. The
browser decodes the response as it arrives over the network in multiple
chunks, and across those chunks the Flight client can surface the tree
to React before a boundary's children row has been processed, so React
reads the still-pending children, suspends, and commits the boundary's
fallback, then swaps in the content once that row is processed. Holding
the stream server-side doesn't fix this, because the bytes still reach
the browser in multiple network chunks; the race is in how the client
processes them. The flash showed up on warm navigations to a
runtime-prefetch route.

This change releases the stream to the browser live and exposes a
dev-only `_revealAfter` promise on the RSC payload to close that race.
Fully buffering the response on the client, so the Flight client
processes it in a single chunk, would avoid the race, but a streaming
navigation can't be buffered without blocking it on all the dynamic
content that streams in after the shell. Instead, the server resolves
the promise once the shell-stage content (the static shell, or the
runtime-prefetchable shell for runtime-prefetch routes) has been flushed
to the stream, or earlier on a cache miss, since a cold render can't be
prod-representative anyway and we would rather let everything stream as
fast as possible. A client navigation gates revealing the response on
it, deferring resolving the response's deferred RSCs until it settles.
Because the promise's resolution row follows the children's row in the
RSC payload, the children's row has already been processed by the time
the client unblocks, so the fallback no longer appears.

The SSR render keeps the original hold. The `_revealAfter` reveal gate
is a client-side mechanism and doesn't apply to the HTML render, which
consumes the same payload to produce the initial HTML and would
otherwise stream a boundary's fallback before its content arrived. For
that consumer the render holds the stream until the shell-stage content
has flushed, so the HTML reflects the prerendered shell rather than a
premature fallback.

A first navigation to a not-yet-known route needs separate handling.
Such a navigation has no prior cache entry, so the server returns the
new subtree's content inline in the navigation response rather than as a
deferred RSC, which means the deferred-RSC gate doesn't cover it.
`navigateToUnknownRoute` now awaits `_revealAfter` (decoded from that
response) before building the navigation tree, so the inline content is
decoded by the time React reads it, the same gate the known-route path
applies to its deferred RSCs.

On the server, `streamStagedRenderInDev` resolves `revealAfter` at the
shell boundary (or earlier on a cache miss) in one task and advances
into the dynamic stage in the next, so the resolution row flushes ahead
of the dynamic chunks and the client unblocks as soon as the shell is
ready rather than only while the dynamic content streams. The
shell-boundary stage, previously `streamReleaseStage`, is renamed
`revealAfterStage` to match the new mechanism.

A regression test in `cache-components-dev-streaming` hard-reloads the
home page and then navigates to the runtime-prefetch route many times,
exercising both the unknown-route inline-seed first navigation and the
known-route deferred-RSC path, and asserts that the private cache's
fallback never enters the DOM. The SSR path remains covered by the
existing `ppr-root-param-fallback` test, which checks that a warm
initial render places the cached content in the shell rather than its
`Suspense` fallback.
2026-06-13 15:47:39 +02:00
Hendrik Liebau 5b0aa04b10 Persist 'use cache: private' entries in dev (#94694)
Private caches were never stored in a cache handler, so every reload in
`next dev` re-ran them from scratch and they registered as a cache miss
on each load. This change persists `'use cache: private'` entries in
development in a dedicated built-in in-memory handler so that warm
reloads are fast. The handler is gated on
`process.env.__NEXT_DEV_SERVER` and is kept out of the kind-keyed
handlers map so it can never be replaced by a user-configured `default`
handler: private cache entries can hold data specific to the incoming
request (for example, derived from its cookies or headers) and must
never reach a remote or otherwise persistent handler. Production keeps
private caches non-persisted as before.

The coarse cache-handler key for a dev private cache is scoped by the
request's cookies and headers so entries for requests with different
request data don't collide. It excludes Next-internal cookies that
aren't application data (the HMR refresh hash, already part of the cache
key, and the instant-navigation cookie, which toggles while a navigation
lock is held) and the transport and content-negotiation headers that
vary between otherwise-equivalent requests (a browser reload adds
`cache-control`, and `accept` and `sec-fetch-*` differ between an HTML
navigation and an RSC request). Keying by only the cookies and headers a
cache actually reads is left as a follow-up; read root params are
already tracked that way, the same as for public caches. The cache life
is forced to `revalidate: 0` with a 5-minute `expire`, so each read
serves the stale entry immediately and warms a fresh one in the
background through the existing stale-while-revalidate path.

Cross-request deduplication now applies to private caches in development
too, so concurrent requests with identical request data share a single
fill; it remains skipped in production where request-specific data must
not be shared across requests. To make this deterministic,
`saveToCacheHandler` resolves the metadata a cross-request joiner awaits
only after the entry has been written to the handler, so the joiner
finds it when re-reading its recomputed key. This closes a pre-existing
race in that path, present for public caches too but never surfaced by
their cross-request test, where the joiner's metadata could resolve
before the handler write had landed.

The defensive invariant that rejected reading a private entry from a
handler is removed: it only existed to narrow `cacheContext.kind` for a
code path that no longer needs it, and dev now legitimately reads
persisted private entries while production never registers a private
handler to read from.
2026-06-11 22:30:08 +02:00
Aurora Scharff 5b99d26df8 instant: polish client-hook overlay wording, cards, and docs links (#94496)
### What?

Polishes the dev-overlay UX for the client-hook prerender error after
Josh's framework fix landed in
[#94494](https://github.com/vercel/next.js/pull/94494). The overlay now
names the hook in the headline and shows per-hook fix cards.

### Why?

Different hooks need different fixes. `useSearchParams` always suspends,
but `generateStaticParams` doesn't apply to it. `useParams` is the only
hook GSP resolves at build time. `usePathname` and
`useSelectedLayoutSegment(s)` need a Suspense boundary or the `[block]`
export.

### How?

- Headline now reads "Next.js encountered URL data `useX()` in a Client
Component outside of `<Suspense>`", matching the body factory wording.
- Per-hook card sets in `instant-guidance-data.ts`: `useSearchParams` →
Stream + Block; `useParams` → Stream + GSP + Block; `usePathname` /
`useSelectedLayoutSegment(s)` → Stream + Block.
- Build-time message in `ClientHookDynamicError` /
`ParamClientHookDynamicError` matches the overlay card set.
- Companion docs page:
[vercel/front#72622](https://github.com/vercel/front/pull/72622).

### Verification

Demo scenarios on the [error-messages-overhaul test
app](https://error-messages-overhaul-ibsl.labs.vercel.dev/):
[88-client-use-pathname](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/88-client-use-pathname),
[89-client-use-search-params](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/89-client-use-search-params),
[90-client-use-params](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/90-client-use-params),
[91-client-use-selected-layout-segment](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/91-client-use-selected-layout-segment),
[92-client-use-selected-layout-segments](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/92-client-use-selected-layout-segments).

<!-- NEXT_JS_LLM_PR -->

---------

Co-authored-by: Josh Story <gnoff@storyposted.com>
2026-06-09 15:20:11 +02:00
Hendrik Liebau 6f4d94ac88 Stream Cache Components dev render instead of restarting on cache miss (#94457)
When Cache Components is enabled, `next dev` previously simulated a
production loading experience on every cold request. The render did a
prospective pass to detect cache misses, and on any miss it waited for
every cache to fill via `cacheSignal.cacheReady()` and then restarted
the render with warm caches before streaming anything, so the browser
saw nothing until the slowest cache had filled. Every cold load blocked
on cache population.

This change replaces the restart-on-cache-miss flow with a single
non-abandoning staged render that streams immediately and fills caches
as a side effect. On a cold load the Suspense fallbacks stream right
away and the cached content resolves as its cache fills; on a warm
reload the staged progression matches the previous no-cache-miss path.
The render is split into clearly owned pieces: `setUpStagedDevRender`
builds the staged controller, cache signal, and resume cache;
`streamStagedRenderInDev{Node,Web}` runs the streaming render and
reports a result once the stream has fully finished; and
`stagedRenderWithCachesInDev{Node,Web}` returns the stream and leaves
the validation follow-up detached so it never blocks the response.

The render advances its stages in sequential tasks, and the stream is
not handed back the instant it exists: it is held until the render has
advanced through the stage whose content belongs in the shell. That is
the static stage for initial loads, HMR refreshes, and plain
navigations, or the runtime stage for client navigations to a route with
a runtime prefetch config, whose runtime-prefetchable content the
navigation's prefetch would have settled. It never waits for the dynamic
stage. Buffering the shell before the first flush keeps the streaming
renderer from emitting a premature Suspense fallback for content that
belongs in the shell, and it mirrors production, where the static shell
(plus runtime-prefetchable content where configured) is served and the
remaining holes stream in as fallbacks.

Two internal reads that would otherwise register as synchronous IO and
wrongly force the render to the dynamic stage, the cache handler's
tag-expiry clock check and the hot reloader's module-scope dev client
id, are now read untracked: via `performance.timeOrigin +
performance.now()` like the `'use cache'` handler, and only in the
browser where the HMR connection reads it, respectively.

Cache Components rules validation now runs in that background follow-up,
once the streamed render has fully settled. `planDevValidation` inspects
the finished render and picks one of three paths: forward an invalid
dynamic usage error the streamed render already recorded and stop (for
example a request API used inside `'use cache'`); validate the streamed
render's own chunks when it neither missed caches nor hit sync IO; or,
when it did either, validate a dedicated warm-cache render instead.
Because that warm render reads the filled caches back rather than
filling them, it can surface an invalid dynamic usage error the cold
streamed render cannot, such as a nested dynamic `use cache` cache life
that propagated to a parent with no explicit `cacheLife`; that error is
forwarded and validation is skipped, just as one recorded by the
streamed render is.

Since cold loads no longer block on cache fills, the transient
cache-status indicator that reflected that wait is no longer emitted; a
follow-up will instead add an indicator that tells the user whether a
render streamed with cache misses, and so wasn't representative of
production.
2026-06-09 10:38:12 +00:00