`next dev` reported missing Suspense boundaries in layouts that the
build accepted. For `/[top]/items/[bottom]`, `generateStaticParams`
returned `[{ top: 't1' }]`, and the page wrapped its `bottom` access in
Suspense. A request for `/t2/items/b2` still reported the layout's
access to `top` as an error. Development treated both parameters as
unresolved because the requested `top` value was not generated, although
the required static shell only needed to defer `bottom`.
Production Cached Navigations used the same overly broad parameter set
and omitted eligible static content from repeat visits. Resumes also
reconstructed that set from the original build manifest, even after an
on-demand prerender had produced a more complete shell.
This replaces the alternative proposed in #98460. That proposal fixes
the development error by selecting a separate fallback parameter set for
validation while keeping response staging unchanged. The two sets are
not intended to differ for the same shell target. Correcting only
validation would preserve the incorrect staging decision and leave
production Cached Navigations without the eligible static content.
Staging and static-shell validation now use one `stagedFallbackParams`
set for each selected shell target. Required partial shells retain their
unresolved parameters, even when a later request can complete them.
Prerenders record their parameter set in postponed state, and resumes
use that recorded set rather than reconstructing it from the generic
source.
Dynamic RSC requests now read and revalidate the completed-shell cache
key, so they find partial artifacts that the fully resolved pathname
lookup missed. Request metadata and `RequestStore` both expose the set
as `stagedFallbackParams`. Action-only fallback detection checks actual
unresolved parameters instead of treating deferred values as missing.
Implements `unstable_prefetch()`, which is intended for use in
`partialPrefetching`. `await unstable_prefetch()` excludes content from
the app shell -- it will only be available when using `prefetch={true}`
(speculative prefetch) or during navigations.
As a rule of thumb, `unstable_prefetch()` resolves whenever static
`params` would:
- in a static prerender
- but NOT the app shell extracted from it, which is param-less
- in a runtime prefetch (`prefetch={true}`)
- but NOT a runtime app shell, which is param-less
Note that `prefetch()` is URL data, so using it in an App Shell without
Suspense will trigger an instant insight.
### Implementation notes
`prefetch()` is treated like URL data, so it resolves in the
`PrefetchStatic/PrefetchRuntime` stages added in #96908. The
implementation is basically analogous to `unstable_navigation()` except
using different stages. i've considered abstracting them into one
implementation, but decided against that for now, we can deduplicate
later.
Error messages about URL data have not been updated to mention it yet --
we will do that as a follow up, along with docs.
`await prefetch()` does not count as a runtime data access, meaning that
it won't affect the static prefetch hint for a route. however `await
prefetch(); await cookies()` does deopt the route, because using a
speculative runtime prefetch would reveal more content. Note that this
may cause us to unnecessarily deopt a shell to runtime even if only the
speculative part of the content would be improved by a runtime request;
this is not a new issue, but it's something we should optimize.
`navigation()` is a new API that allows omitting contents from runtime
shells and runtime prefetches. Conceptually, the point is to express
that something is expensive to compute, so we shouldn't do it for
requests that may not get used (shells and prefetches). Notably, this
means that it's fine to include it in a static prerender -- it'll be
computed once and used for many requests, so it doesn't make sense to
exclude it.
## Implementation
The split in behavior across static and runtime prerenders is a
departure from how most of our APIs behave -- usually, if something
resolves statically, then it also resolves in "more complete" prerender.
Departing from this leads to some implementation complexity.
We include three new stages, used by two facets of the implementation:
```diff
export enum RenderStage {
Before = 1,
//
ShellStatic = 10,
+ PrefetchStatic = 11, <------- params, prefetch() [static prerenders]
+ NavigationStatic = 12 <------navigation() [static prerenders]
Static = 13, <--------------- finish accumulators [static prerenders]
//
ShellRuntime = 20,
Runtime = 21, <-------------- params, prefetch() [runtime prerenders]
+ NavigationRuntime = 22, <---- navigation() [runtime prerenders]
//
Dynamic = 30,
Abandoned = 40,
}
```
### NavigationRuntime
In runtime prerenders (or dev renders that simulate them),
`navigation()` resolves in `NavigationRuntime`
We only reach this stage in 1. the embedded runtime prerender produced
for Cached Navigations and 2. during dev/prod full staged renders --
runtime shells end in `ShellRuntime`, and runtime prefetches end in
`Runtime`.
Notably, this means that content gated behind `navigation()` is included
in the embedded runtime prefetch stream.
### PrefetchStatic & NavigationStatic
This is a helper stage added before `Static`. Static prefetches still
use the `Static` stage for their output. This new stage exists so that
we can resolve static `params` (and `prefetch()` when we implement it)
which the stage is named after) separately from `navigation()`, which
resolves in `NavigationStatic`, after which the prerender ends in
`Static`. This separation is important, because during static prerenders
we track whether or not runtime APIs are used (see
`trackRuntimeDataAccessed`) to determine if a runtime shell (or runtime
prefetch) might give us more content than the static ones. However, a
runtime shell/prefethc **would not resolve navigation()**, so `await
navigation(); await cookies()` would not reveal more content, and thus
shouldn't count as a usage that prevents static optimization.
We achieve this by checking the stage inside
`trackRuntimeDataAccessedImpl` and not tracking anything if we reached
the `NavigationStatic` stage.
### Behavior of shells and validation
As noted before, `navigation()` has an incompatible resolution order
between static and runtime prerenders. In #97040, we did some groundwork
to deal with this in validation.
Static prerenders resolve `navigation()`, which means that static shells
include content gated behind navigation(). This means that Static Shell
Validation allows them.
On the other hand, App shells **do not** resolve `navigation()`. This
leads to an inconsistency for Instant Validation -- a `await
navigation()` might be fine if a page is prefetched statically, but
would become blocking as soon as the page starts using runtime data and
switches to a runtime shell. To avoid this pitfall, we pessimistically
assume that any `navigation()` _might_ be part of a runtime
shell/prefetch, so any `navigation()` unguarded by Suspense will error
in IV.
In practice, this is handled analogously to static params: we do a dev
render with `needsAppShell: true`, which makes `navigation()` resolve in
`NavigationRuntime`, and then we use the `ShellRuntime` stage when
validating, which means that `navigation()` will be a hole. Note that
the discriminated error message logic currently only retries errors
using the `Runtime` stage, which won't have `navigation()` resolved
either, so it will be incorrectly reported as dynamic data. This will be
improved in a follow up.
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.
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
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.
Follow-up to #95151, implementing support for validating awaits of
static params (i.e. when using `generateStaticParams`)
An app shell cannot contain any link data. This poses a challenge when
we're trying to re-use the dev render for instant validation, because we
have to make a choice:
1. either we resolve static params (from `generateStaticParams`) in the
`Static` stage, and get an accurate *static HTML shell* (used for the
initial navigation), but an incorrect app shell
2. or we resolve them in the `ShellRuntime` stage and we get an accurate
App Shell, but cannot validate a static HTML shell (which would contain
static params)
We'll use order 2 in the main render whenever client-navigating to a
page with `partialPrefetching` enabled. We still need a render with
order 1 for Static Shell Validation, so we perform a second, partial
render that aborts before the dynamic stage (because only instant
navigation needs the dynamic stage). As an optimization, we can skip the
secondary render if the page doesn't have static params, because in that
case the two orders are equivalent.
If we're performing an initial load, we'll use order 1 (to reflect the
HTML shell). in this case, we'll do a full secondary render with order
2. Same as above, we can skip the secondary render if there's no static
params.
---
Where params resolve (order 1 vs order 2) is controlled by
`requestStore.needsSessionShell`. We then end up with two sets of
"validation inputs" (mainly rendered chunks) that we can feed into
static and instant validation respectively.
Also note that this PR doesn't touch `environmentName`, because that
required changing too many tests. This will be addressed in a follow-up.
When Cache Components is enabled, the development server threads a
`fallbackParams` request meta for dynamic app routes so the staged
render knows which params are not statically known and must be deferred
to a later stage. The previous computation walked the prerendered routes
from `getStaticPaths` and kept the one with the fewest fallback params,
without checking that the route actually matched the requested URL.
Consider `/mixed/[lang]/[id]` where `generateStaticParams` covers `lang:
'en'` but not `id`: the prerendered routes are the base
`/mixed/[lang]/[id]`, which defers `[lang, id]`, and the covered
`/mixed/en/[id]`, which defers only `[id]`. For the request
`/mixed/fr/123` the fewest-fallback route is `/mixed/en/[id]`, but `en`
does not match `fr`, so applying its `[id]` set left `lang` out of the
fallback set and `fr` was treated as a statically known value.
Because this meta decides which stage each param resolves in, and the
stage decides the environment a replayed `console.log` is attributed to,
treating `fr` as static resolved it in the prerender stage instead of
deferring it to the runtime stage. The computation now matches the
requested URL against each prerendered route with the canonical
`getRouteRegex` and, among the routes that match, picks the
most-specific one, the one with the fewest fallback params. For
`/mixed/fr/123` only the base route matches, so its `[lang, id]` set is
used and both params defer, while for `/mixed/en/123` the covered
`/mixed/en/[id]` still matches and wins, so `lang` resolves statically
and only `id` defers. This mirrors what a production build writes to the
prerender manifest, where the server matches the URL to the
most-specific prerendered route at request time. The change is
development-only, gated on the route module being in dev mode, and
production continues to read the manifest. A later change in this stack
reads the same `fallbackParams` meta for the Instant Navigation testing
API's on-demand shell render, so that path defers the identical per-URL
set a production prefetch would.
In #[94645](https://github.com/vercel/next.js/pull/94645) we discovered
that there's a disparity between how we handle caches with low stale
time (<30s): runtime prerenders exclude them, but static prerenders do
not. We should avoid situations where a static prerender contains data
that a runtime prerender of the same page doesn't. Also, the motivation
is the same in both cases: if something goes stale that quickly, it's
generally not worth prefetching.
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.
The default in-memory cache handler drops an entry once it goes stale
(past its `revalidate` time) rather than serving it: in-memory entries
are fragile, so warming a replacement in the background isn't worth it
when it's likely to be evicted before it's used, and we don't want to
keep reusing a stale entry for too long either. With this change the dev
server (`next dev`) makes an exception and serves the stale entry until
it actually expires, using `expire` as the drop threshold instead of
`revalidate` and relying on the wrapper's existing
stale-while-revalidate path to warm a fresh entry in the background.
Production (`next start`) and `next build --debug-prerender` keep the
previous drop-at-`revalidate` behavior, so the change is gated on
`process.env.__NEXT_DEV_SERVER`. The tag-based discard is preserved, so
`revalidateTag` still invalidates in dev.
This change has two motivations. It keeps the dev inner loop fast,
turning a warm reload of a short-lived cache into an immediate hit
instead of a cold miss that re-runs the cache. It is also the foundation
for upcoming dev-only caching changes that depend on the handler serving
stale entries: persisting `'use cache: private'` entries in dev, and
keeping `cacheMaxMemorySize: 0` fast in dev. Both serve a previous value
while warming a fresh one in the background, which only works once the
handler stops dropping stale entries in dev.
This also lets us drop the workarounds that several Cache Components dev
fixtures used to isolate themselves from the handler dropping the entry
at `revalidate`. The short-lived fixtures go back to
`cacheLife('seconds')` and the short-stale fixtures drop their long
`revalidate`. Their warm reloads are now stale hits served by the dev
server, which is the behavior these tests are meant to exercise and
which removes the short-lived hit/miss flakiness at its source.
A new `use-cache` test pins the behavior directly: in dev a reload after
`revalidate` serves the previous value immediately and warms a fresh one
in the background, while a `next start` build of the same fixture
re-runs the cache and shows a fresh value.
When the streaming dev render defers a short-lived `'use cache'` entry
to a later stage (a `revalidate` of zero or a short `expire` excludes it
from the static shell, a short `stale` time excludes it from the runtime
prefetch shell), it previously left the cache signal read open. At a
staged rendering task boundary that open read looked like a pending
cache read, so even a warm handler hit was counted as a cache miss,
lighting the cold-cache indicator and routing validation through the
background warm render. We now end the cache signal read as soon as such
an entry is deferred, the same as the prerender path already does, and
serve the buffered value through a plain stream instead of one tracked
by the cache signal, so a warm short-lived hit is no longer mistaken for
a miss. A deferred entry can still turn out to be stale and need
regenerating (for instance, a cache handler may return a past-`expire`
entry), so the regenerate path re-begins the read before generating,
keeping the signal balanced against the regeneration's own end of the
read rather than over-decrementing it.
The runtime-prefetch exclusion for a short stale time must only apply
when the render actually produces the runtime prefetch shell, which is a
property of the request rather than the cache entry. An initial HTML
load, a plain client navigation, and an HMR refresh all produce the
static shell, where a short-stale entry stays, mirroring the static
prerender. A client navigation into a runtime-prefetch route produces
the runtime prefetch shell, where it is excluded, mirroring the
`prerender-runtime` prerender. We thread the render's shell stage onto
the request store, and onto the warm validation render that mirrors it,
and gate the stale-based deferral on `shellStage === Runtime`. This
preserves the build-time asymmetry where a short stale time is omitted
from the runtime prefetch but not from the static shell.
A new `short-stale-cache` fixture and warmup test exercise this
asymmetry directly: a `'use cache'` entry with a short stale time but a
long expire resolves in the static stage on an initial load and on a
navigation without runtime prefetch, and only resolves dynamically on a
navigation into a runtime-prefetch route. The previously skipped
short-lived warmup case is re-enabled, and the cache-indicator
short-lived case now asserts the cold-cache badge on a cold load but not
on a warm reload. Those fixtures use a long `revalidate` so the entry
stays a fresh hit for the duration of the test, isolating them from the
in-memory handler dropping the entry at `revalidate`; that workaround
can be dropped once the cache handler serves stale entries in dev.
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.
While force-runtime does opt you into runtime prefetching today (i.e. it
does force) the intended semantic is shifting to convey that the Segment
itself is designed for and makes sense (i.e. cost / performance
tradeoff) to runtime prefetch. In the future segments that may be
runtime prefetched might not be for various optimization reasons. We
therefore are renaming the option from force-runtime to allow-runtime.
In the future if we change allow-runtime to sometimes not runtime
prefetch we can always ship a new force-runtime as a codemod option that
recovers the current behavior.
This wording also better demonstrates why this is a feature of the
Segment and not say an option on the link like `<Link
prefetch="force-runtime" />`.
### What?
Migrate remaining direct `next-webdriver` test callers that have a
`NextInstance` to `next.browser()`, and expose the shared `Playwright`
browser type from `e2e-utils`.
### Why?
`NextInstance.browser` should be the supported browser-opening interface
for test fixtures, with `next-webdriver` kept as the private
implementation detail.
### How?
Updated affected development, e2e, and production tests to call
`next.browser()` directly, passing `baseUrl` where tests intentionally
target a manually spawned or proxied server. Shared helpers now receive
browser callbacks from the test context, and browser types import
`Playwright` from `e2e-utils` instead of deriving from `next.browser` or
importing from private paths.
<!-- NEXT_JS_LLM_PR -->
prefetching is now controlled with the prefetch export and this option
is inert on the instant export. this removes the prefetch option as a
valid property on the instant config type.
also updates the ts plugin for this export and adds the definition for
the previously landed prefetch export
"auto" mode is the default. It does not need to be explicitly exported
"force-" modes suggest overriding framework heuristics "force-disabled"
disables prefetching for this segment "force-static" forces any
prefetching of this segment to be static "force-runtime" forces any
prefetching of this segment to do runtime prefetching
It's worth noting that when runtime prefetching we fetch the necessary
segment and all child segments in a single request. This means that a
deeper segment might specify disabled or static and still get
conditionally rendered as a runtime prefetch. This was already the
behavior of runtime prefetching and not changing in this PR just
something to call out since it may be confusing to folks trying to
understand how the implementation of prefetching actually work
Allows you to opt into runtime prefetching without coupling it to the
way you configure instant validation. We do not intend to allow shipping
runtime prefetching without opting into instant validation unless you
specifically disable the validation because the cost of the runtime
prefetching is potentially high but the way you configure validation is
likely going to be pulling in lots of test code and we want to make it
easier to discern that the validation code is not going to be bundled
into production builds so separating the config is helpful.
Another reason to separate the config is that we expect that eventually
most prefetching is configured globally and you do not need to opt
specific segments into a different prefetching strategy.
Improved client component error messages to accurately describe the
constraint: these are route segment configs that require a Server
Component module, not exports that are forbidden.
Mostly mechanical rename.
Also changes the error page to `errors/invalid-instant-configuration`.
I'm not really worried about dangling links here because this is a new
API and we don't expect anyone to be using it yet.
Prior to this change any "hole" in a prerender that would block the
shell was considered an error and you would be presented with a very
generic message explaining all the different ways you could have failed
this validation check.
With this change we use a new technique to validate the static shell
which can now tell the difference between waiting on uncached data or
runtime data. It also improves the heuristics around generateMetadata
and generateViewport errors.
Added new error pages for runtime sync IO and ensure we only validate
sync IO after runtime data if the page will be validating runtime
prefetches.
Restored the validation on HMR update so you can get feedback after
saving a new file.
---
We've also discovered that hanging inputs are not handled correctly.
Fixing this is non-trivial and will be done in a follow-up, so for now,
we're disabling the failing tests.
---------
Co-authored-by: Josh Story <story@hey.com>
Co-authored-by: Hendrik Liebau <mail@hendrik-liebau.de>
Tweaks the environment label logic to only use label things as
`Prefetch` if there's a runtime prefetch config. If there's none, we
label it as `Prefetchable` instead.
---------
Co-authored-by: Josh Story <story@hey.com>
This moves `experimental.cacheComponents` to a top level config. As part
of this, I disabled some tests in `build-output-prerender` that assert
on `cacheComponents` appearing in the experimental list. In a separate
PR, I'm going to show that Cache Components is enabled next to the
bundler info.
This also updates some docs pages to remove "experimental" language.
This PR extends dev rendering with an extra stage - Runtime, where we
resolve Runtime APIs (`cookies()`, `params`, etc), but not dynamic APIs
(e.g. `connection()`, uncached fetches). This separation requires some
changes to `makeDevtoolsIOPromise` and other tasky delay code that we've
had in place before.|
I've implemented this as `StagedRenderingController` (generally accessed
via `requestStore.stagedRendering`), which allows creating promises that
resolve in the correct stage.
```ts
stagedRendering.delayUntilStage(RenderStage.Runtime, cookiesObject) // resolves in the runtime stage
stagedRendering.delayUntilStage(RenderStage.Dynamic, somethingDynamic) // resolves in the dynamic stage
```
Notably, in the case where the initial render has cache misses and we
only use it as a cache warmup, we can also use this to _not_ resolve
dynamic promises, which saves us from doing some unncecessary (uncached)
work. This means that, if the render ends up being a warmup, dynamic
promises can be left hanging (and later aborted), similar to how
`makeHangingPromise` works in a prerender.
For simplicity and compatibility with other codepaths, most callsites
that need a staged promise still use `makeDevtoolsIOAwarePromise`, but
now they pass in a stage argument to control the timing if we are
rendering with stages.
---
Based on the stage, we now add a third environment label -- "Prefetch",
to correspond with runtime prefetches. This label is currently applied
even if there's no prefetch config in place, which is a bit confusing,
and will be addressed in a follow-up (if there's no runtime prefetch
config, we want to label runtime logs as "Server" even if they run in a
separate stage).
This PR changes the timing used in `CacheSignal.cacheReady()` to work
correctly when rendering, not just prerendering. When rendering, React
schedules new work in `setImmediate` (as opposed to using a microtask
when prerendering), and `CacheSignal.cacheReady()` needs to account for
that, because we need to wait longer to make sure that react had time to
render and thus we've started all the relevant cache reads.
Note that this PR increases the delay for both prerender _and_ render.
This is to make it easier to reason about (by not having separate
codepaths), but also to not be so tightly coupled to React pinging work
in a microtask when prerendering, in case that changes in the future.
This PR replaces the previous approach to the dev-time cache warmup. On
full-page requests, we
1. We attempt an initial RSC render. It uses a RequestStore, but
includes a `cacheSignal` and a `prerenderResumeDataCache` to be filled
1. if there's no cache misses, we use the RSC render as is, and move
onto SSR
2. If there's any cache misses in the static stage (i.e. during the
first timeout), we treat the inital render as a prospective render, use
it only for filling caches, and discard the result
3. Once caches are filled, we render RSC again (using a fresh
RequestStore, with a filled `renderResumeDataCache`), and use this
second stream for SSR instead.
With this strategy, we minimize the amount of work we need to do for
cache warming -- once caches for a page are filled, we can render it in
one go, with no separate cache-filling render necessary.
---
A lot of the effort here goes into trying to reflect the behavior of a
static prerender into what we do during a dynamic render -- if something
would be a dynamic hole (hanging promise) in a prerender, we shouldn't
resolve it microtaskily (in the static stage). Instead, we have to delay
it into a future timeout (the dynamic stage). In this PR, we're still
using `makeDevtoolsIOAwarePromise` for this (i.e. just `new
Promise((resolve) => setTimeout(resolve))`), though this will change to
a more precise mechanism in #84644.
The timing of when promises resolve is currently tested in
`cache-components.dev-warmup.test.ts`, where we check the environment
labels on the server logs replayed in the browser, and use that to
verify which "phase" (Static/Dynamic) a given API resolves in.
This will be the foundation for prefetch validation, where we'll need to
snapshot what was rendered in each stage (Static/Runtime/Dynamic) and
use that to validate whether a prefetch would result in an instant
navigation.
Note that there's currently a bug involving `params` and `searchParams`
-- they can currently incorrectly resolve in the static phase (because
those promises are created before we start the actual render). We're not
(yet) relying on the timing of these promises for anything critical, so
it's fine to leave it for now. This bug will be addressed in #84644,
where I introduce a more precise mechanism for controlling the timing of
promise resolution, which also lets us separate "runtime" APIs like
`cookies` into a separate phase.
---
I've also left the current `spawnDynamicValidationInDev` codepath as is,
so after the we're done with all the render restarting, we'll still kick
off a validation prerender. This will also change in the future (and
could be optimized -- we've already ensured that all the caches are
filled, so we could e.g. skip the prospective render there) but I'm
trying not to do everything at once.
---------
Co-authored-by: Josh Story <story@hey.com>
## What?
Rename `experimental.dynamicIO` to `experimental.cacheComponents` across
the Next.js codebase.
## Why?
We're going to be merging the functionality of the `ppr`, `dynamicIO`
and `useCache` experimental flags into the singular `cacheComponents`
flag to reduce complexity of the codebase and simplify adoption for
users wanting to experiment with experimental features.
## How?
- Renamed the configuration option from `experimental.dynamicIO` to
`experimental.cacheComponents`
- Added deprecation handling with automatic migration for the old option
name
- Updated all documentation, tests, and internal references
- Updated Rust code in SWC transforms and Turbopack
- Maintained backward compatibility with deprecation warnings
NAR-158