Skip to content

Commit

Permalink
[Segment Cache] Support <Link prefetch={true}>
Browse files Browse the repository at this point in the history
This implements support in the Segment Cache for "full" link prefetches,
typically initiated by passing `prefetch={true}` to a `<Link>`.

The goal of a full prefetch is to request all the data needed for a
navigation, both static and dynamic, so that once the navigation occurs,
the router does not need to fetch any additional data. So, a full
prefetch implicitly instructs the client cache to treat the response as
static, even the dynamic data.

The implementation is largely the same as what we do to support non-PPR
-enabled routes — a request tree is sent to the server describing
which segments are already cached and which ones need to be rendered.
While constructing the request tree, if all the segments are already
in the client cache, the request can be skipped entirely. The main
difference is that a full prefetch will only reuse a cached segment
entry if it does not contain any dynamic holes.
  • Loading branch information
acdlite committed Dec 28, 2024
1 parent 1c4e10c commit ea6eba2
Show file tree
Hide file tree
Showing 13 changed files with 814 additions and 254 deletions.
5 changes: 3 additions & 2 deletions packages/next/src/client/components/app-router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -280,11 +280,12 @@ function Router({
? // Unlike the old implementation, the Segment Cache doesn't store its
// data in the router reducer state; it writes into a global mutable
// cache. So we don't need to dispatch an action.
(href) =>
(href, options) =>
prefetchWithSegmentCache(
href,
actionQueue.state.nextUrl,
actionQueue.state.tree
actionQueue.state.tree,
options?.kind === PrefetchKind.FULL
)
: (href, options) => {
// Use the old prefetch implementation.
Expand Down
19 changes: 17 additions & 2 deletions packages/next/src/client/components/segment-cache/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ export type RouteCacheEntry =

export const enum FetchStrategy {
PPR,
Full,
LoadingBoundary,
}

Expand Down Expand Up @@ -539,6 +540,15 @@ function clearRevalidatingSegmentFromOwner(owner: SegmentCacheEntry): void {
}
}

export function resetRevalidatingSegmentEntry(
owner: SegmentCacheEntry
): EmptySegmentCacheEntry {
clearRevalidatingSegmentFromOwner(owner)
const emptyEntry = createDetachedSegmentCacheEntry(owner.staleAt)
owner.revalidating = emptyEntry
return emptyEntry
}

function onRouteLRUEviction(entry: RouteCacheEntry): void {
// The LRU evicted this entry. Remove it from the map.
const keypath = entry.keypath
Expand Down Expand Up @@ -995,7 +1005,7 @@ export async function fetchSegmentOnCacheMiss(
}
}

export async function fetchSegmentPrefetchesForPPRDisabledRoute(
export async function fetchSegmentPrefetchesUsingDynamicRequest(
task: PrefetchTask,
route: FulfilledRouteCacheEntry,
dynamicRequestTree: FlightRouterState,
Expand All @@ -1005,14 +1015,19 @@ export async function fetchSegmentPrefetchesForPPRDisabledRoute(
const nextUrl = task.key.nextUrl
const headers: RequestHeaders = {
[RSC_HEADER]: '1',
[NEXT_ROUTER_PREFETCH_HEADER]: '1',
[NEXT_ROUTER_STATE_TREE_HEADER]: encodeURIComponent(
JSON.stringify(dynamicRequestTree)
),
}
if (nextUrl !== null) {
headers[NEXT_URL] = nextUrl
}
if (!task.includeDynamicData) {
// Although this is a dynamic request, the server should not include
// dynamic data in the response. This happens when PPR is disabled — we
// prefetch up to the nearest loading boundary.
headers[NEXT_ROUTER_PREFETCH_HEADER] = '1'
}
try {
const response = await fetchPrefetchResponse(href, headers)
if (!response || !response.ok || !response.body) {
Expand Down
11 changes: 9 additions & 2 deletions packages/next/src/client/components/segment-cache/prefetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,24 @@ import { schedulePrefetchTask } from './scheduler'
* Entrypoint for prefetching a URL into the Segment Cache.
* @param href - The URL to prefetch. Typically this will come from a <Link>,
* or router.prefetch. It must be validated before we attempt to prefetch it.
* @param nextUrl - A special header used by the server for interception routes.
* Roughly corresponds to the current URL.
* @param treeAtTimeOfPrefetch - The FlightRouterState at the time the prefetch
* was requested. This is only used when PPR is disabled.
* @param includeDynamicData - Whether to prefetch dynamic data, in addition to
* static data. This is used by <Link prefetch={true}>.
*/
export function prefetch(
href: string,
nextUrl: string | null,
treeAtTimeOfPrefetch: FlightRouterState
treeAtTimeOfPrefetch: FlightRouterState,
includeDynamicData: boolean
) {
const url = createPrefetchURL(href)
if (url === null) {
// This href should not be prefetched.
return
}
const cacheKey = createCacheKey(url.href, nextUrl)
schedulePrefetchTask(cacheKey, treeAtTimeOfPrefetch)
schedulePrefetchTask(cacheKey, treeAtTimeOfPrefetch, includeDynamicData)
}
Loading

0 comments on commit ea6eba2

Please sign in to comment.