Skip to content

[pull] canary from vercel:canary #241

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Aug 1, 2025
Merged

[pull] canary from vercel:canary #241

merged 4 commits into from
Aug 1, 2025

Conversation

pull[bot]
Copy link

@pull pull bot commented Aug 1, 2025

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.3)

Can you help keep this open source service alive? 💖 Please sponsor : )

nextjs-bot and others added 4 commits July 31, 2025 22:25
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
vercel/vercel#13547. It was released in
`[email protected]`

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 is part of a larger effort to remove dynamic params from server
responses except in cases where they are needed to render a Server
Component. If a param is not rendered by a Server Component, then it can
be omitted from the cache key, and cached responses can be reused across
pages with different param values.

In this step, I've implemented client parsing of the params from the
response headers.

The basic approach is to split the URL into parts, then traverse the
route tree to pick off the param values, taking care to skip over things
like interception routes.

Notably, this is not how the server parses param values. The server gets
the params from the regex that's also used for routing. Originally, I
thought I'd send the regex to the client and use it there, too. However,
this ended up being needlessly complicated, because the server regexes
are also used for things like interception route matching. But this is
already encapsulated by the structure of the route tree. So it's easier
to just walk the tree and pick off the params.

My main hesitation is that this introduces some risk of drift between
the server and client params parsing; we'll need to keep them in sync.
However, I think the solution is actually to update the server to also
pick the params off the URL, rather than use the ones passed from the
base router. It's conceptually cleaner, since it's less likely that
extra concerns from the base server leaking into the application layer.
As an example, compare the code needed to get a catchall param value in
the
[client](https://github.com/acdlite/next.js/blob/09b16a816ff4d595b5111cb1a6de540838caf97e/packages/next/src/client/client-params.ts#L57-L61)
versus the
[server](https://github.com/acdlite/next.js/blob/09b16a816ff4d595b5111cb1a6de540838caf97e/packages/next/src/shared/lib/router/utils/get-dynamic-param.ts#L37-L84)
implementation.

Note: Although the ultimate goal is to remove the dynamic params from
the response body (e.g. FlightRouterState), I have not done so yet this
PR. The rest of the work will be split up into multiple subsequent PRs.
@pull pull bot locked and limited conversation to collaborators Aug 1, 2025
@pull pull bot added the ⤵️ pull label Aug 1, 2025
@pull pull bot merged commit f643100 into code:canary Aug 1, 2025
5 of 6 checks passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants