`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.
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.
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.
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" />`.
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>