[Cache Components] Runtime prefetching (#81088)
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.