Initial implementation of runtime prefetching.
A "runtime prefetch" is a more complete version of a static prefetch
(i.e. the one that a link does by default), rendered on demand, and not
cached server-side. It will render the server part of the page
dynamically, allowing the usage of
- `params` and `searchParams`
- `cookies()`
- `"use cache: private"` (these are omitted from static prerenders)
- `"use cache"` with a short expire time (these are omitted from static
prerenders)
The result may be partial (in the PPR sense). It will exclude any parts
of the page that depend on
- uncached IO
- `connection()`, `headers()`
This allows the client router to cache the result, because it has a
well-defined stale time. Note that public caches with a stale time below
a certain fixed treshold will also be excluded, because it wouldn't make
sense to keep them around in the router cache if we need to throw them
away soon after getting them.
With this PR, `<Link prefetch={true}>` changes meaning
if `clientSegmentCache` + `cacheComponents` are enabled. It will now
initiate a runtime prefetch instead of a "full" prefetch, which included
everything that a navigation request would. Full prefetches can be done
via `<Link prefetch="unstable_forceStale">`. If only one of the two
flags is on, the behavior of `<Link prefetch={true}>` is unchanged from
how it currently works.
I've split the changes up into separate commits for ease of review:
1. Introducing the new workUnitStore type
2. Server - handling the prefetch header and rendering
3. Client - Link and segment cache changes
### Implementation notes
The client router sends `next-router-prefetch: 2` to signal that it
wants a runtime prefetch (as opposed to the old `next-router-prefetch:
1`, which is used for static prefetches).
> NOTE: this builder change is required for this to work on vercel
https://github.com/vercel/vercel/pull/13547. It was released in
`vercel@44.6.5`
Somewhat confusingly, in order to to avoid existing static prefetch
codepaths, we need the server to _not_ treat this as "a prefetch
request". Instead, we want to mostly treat this like we would a
navigation request, and render dynamically. This means that:
- `isPrefetchRequest` (from `parseRequestHeaders` in `app-render`) will
be `false`
- `getRequestMeta(req, 'isPrefetchRSCRequest')` won't be set
This is a bit ugly but it works for now. I'll try to clean it up in the
future.
We render a payload of the same shape as a navigation request (including
omitting shared layouts, as instructed by the `Next-Router-State-Tree`
header). But unlike a navigation request, we do a cache-components-style
prerender at runtime in order to exclude uncached/sync IO.
This prerender uses a new workUnitStore type, `'prerender-runtime'`.
This store type changes the behavior of `cookies()`, `params`,
`searchParams`, `"use cache: private"`, `next/root-params`, and others.
Unlike a static prerender, if we detect a bad uncached/sync IO usage, we
just log an error (instead of throwing it and erroring) and respond with
whatever we managed to render up until the render was aborted, in hopes
that we can still return something useful to the client. This request is
happening at runtime, so we should try to handle errors gracefully.
We track whether or not the prerender has any dynamic holes, and if it
does, set `x-nextjs-postponed: 1` on the response. This tells the client
router if we still need to fetch more data when navigating, or if we can
skip it because we already have a complete page. Ideally, we'd track
this information per-segment for better reuse on the client side, but
that's not in scope for this PR.
We also set the `x-nextjs-staletime` header on the response to tell the
client router how long it should keep this prefetch in the cache. Note
that this does not affect `Cache-Control`, which should still be the
same as a dynamic navigation request to prevent it from being cached by
anything other than the client router.
This may be improved in the future if it turns out we can safely set an
appropriate `Cache-Control: private, ...` that also accounts for e.g.
changing cookie values, but i'm erring on the side of caution for now.
This commit adds an `onInvalidate` callback to `router.prefetch()` so
custom `<Link>` implementations can re-prefetch data when it becomes
stale.
The callback is invoked when the data associated with the prefetch may
have been invalidated (e.g. by `revalidatePath` or `revalidateTag`).
This is not a live subscription and should not be treated as one. It's a
one-time callback per prefetch request that acts as a signal: "If you
care about the freshness of this data, now would be a good time to
re-prefetch."
The supported use case is for advanced clients who opt out of rendering
the built-in `<Link>` component (e.g. to customize visibility tracking
or polling behavior) but still want to retain proper cache integration.
When the callback is fired, the component can trigger a new call to
`router.prefetch()` with the same parameters, including a new
`onInvalidate` callback to continue the cycle.
(For reference, `<Link>` handles this automatically. This API exists to
give custom implementations access to the same underlying behavior.)
Note that the callback *may* be invoked even if the prefetched data is
still cached. This is intentional—prefetching in the app router is a
pull-based mechanism, not a push-based one. Rather than subscribing to
the lifecycle of specific cache entries, the app occasionally polls the
prefetch layer to check for missing or stale data.
Calling `router.prefetch()` does not necessarily result in a network
request. If the data is already cached, the call is a no-op. This makes
polling a practical way to check cache freshness over time without
incurring unnecessary requests.
This updates the Form component to use the same visibility tracking
implementation as the Link component.
It's basically the same changes that were introduced in
https://github.com/vercel/next.js/pull/74670. The resulting improvements
are the same as those for Link:
- Prefetches are initiated directly inside the IntersectionObserver, not
in a useEffect.
- Prefetches initiated by a form component will be canceled if the form
exits the viewport (if the Segment Cache is enabled).
- Prefetches will be re-triggered when the cache is invalidated (if the
Segment Cache is enabled).
The result of certain kinds of prefetches may depend on the current URL
at the time it is initiated. This is true if for links that are
intercepted, or links that are prefetched using a dynamic request (e.g.
`prefetch={true}`). So, whenever the location changes, we should
re-prefetch all the links using the updated value.
In most cases, this will not result in any new network requests — only
if the prefetch result actually varies on one of these inputs.
For similar reasons, we also re-prefetch links whenever the client cache
is revalidated by a Server Action.
As part of the implementation, we must be able to enumerate over all the
currently visible links. I've added a Set to track this.
This implements evicting the client cache when a Server Action calls
revalidatePath or revalidateTag.
Similar to the old prefetching implementation, it works by clearing the
entire client cache, as opposed to only the affected path or tags. There
are more changes needed on the server before we can support granular
cache eviction. This just gets us to parity with the status quo.