Skip to content

Commit

Permalink
Respect stale time header in client prefetch cache
Browse files Browse the repository at this point in the history
When sending a statically generated response, the server includes the
stale time of the data as a response header. This updates the client
prefetch cache to use this value to determine when the data is stale.

When available, the stale time sent by the server takes precedence over
all other existing heuristics.

In the future, when PPR is enabled, the server-generated value should
*always* be available during a prefetch, unless the server prefetch
hasn't received any data by the time the  navigation happens, in which
case we should skip the prefetch anyway.
  • Loading branch information
acdlite committed Oct 14, 2024
1 parent 8388d23 commit 84ab019
Show file tree
Hide file tree
Showing 4 changed files with 33 additions and 0 deletions.
2 changes: 2 additions & 0 deletions packages/next/src/client/components/app-router-headers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,7 @@ export const FLIGHT_HEADERS = [

export const NEXT_RSC_UNION_QUERY = '_rsc' as const

// TODO: Rebase on Seb's PR
export const NEXT_ROUTER_STALE_TIME_HEADER = 'x-nextjs-stale-time' as const
export const NEXT_DID_POSTPONE_HEADER = 'x-nextjs-postponed' as const
export const NEXT_IS_PRERENDER_HEADER = 'x-nextjs-prerender' as const
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
RSC_CONTENT_TYPE_HEADER,
NEXT_HMR_REFRESH_HEADER,
NEXT_DID_POSTPONE_HEADER,
NEXT_ROUTER_STALE_TIME_HEADER,
} from '../app-router-headers'
import { callServer } from '../../app-call-server'
import { findSourceMapURL } from '../../app-find-source-map-url'
Expand All @@ -48,6 +49,7 @@ export type FetchServerResponseResult = {
couldBeIntercepted: boolean
prerendered: boolean
postponed: boolean
staleTime: number
}

function urlToUrlWithoutFlightMarker(url: string): URL {
Expand All @@ -74,6 +76,7 @@ function doMpaNavigation(url: string): FetchServerResponseResult {
couldBeIntercepted: false,
prerendered: false,
postponed: false,
staleTime: -1,
}
}

Expand Down Expand Up @@ -177,6 +180,9 @@ export async function fetchServerResponse(
const contentType = res.headers.get('content-type') || ''
const interception = !!res.headers.get('vary')?.includes(NEXT_URL)
const postponed = !!res.headers.get(NEXT_DID_POSTPONE_HEADER)
const staleTimeHeader = res.headers.get(NEXT_ROUTER_STALE_TIME_HEADER)
const staleTime =
staleTimeHeader !== null ? parseInt(staleTimeHeader, 10) : -1
let isFlightResponse = contentType.startsWith(RSC_CONTENT_TYPE_HEADER)

if (process.env.NODE_ENV === 'production') {
Expand Down Expand Up @@ -222,6 +228,7 @@ export async function fetchServerResponse(
couldBeIntercepted: interception,
prerendered: response.S,
postponed,
staleTime,
}
} catch (err) {
console.error(
Expand All @@ -237,6 +244,7 @@ export async function fetchServerResponse(
couldBeIntercepted: false,
prerendered: false,
postponed: false,
staleTime: -1,
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@ export function createSeededPrefetchCacheEntry({
kind,
prefetchTime: Date.now(),
lastUsedTime: Date.now(),
staleTime: -1,
key: prefetchCacheKey,
status: PrefetchCacheEntryStatus.fresh,
url,
Expand Down Expand Up @@ -360,6 +361,11 @@ function createLazyPrefetchEntry({
)
if (existingCacheEntry) {
existingCacheEntry.kind = PrefetchKind.FULL
if (prefetchResponse.staleTime !== -1) {
// This is the stale time that was collected by the server during
// static generation. Use this in place of the default stale time.
existingCacheEntry.staleTime = prefetchResponse.staleTime
}
}
}

Expand All @@ -373,6 +379,7 @@ function createLazyPrefetchEntry({
kind,
prefetchTime: Date.now(),
lastUsedTime: null,
staleTime: -1,
key: prefetchCacheKey,
status: PrefetchCacheEntryStatus.fresh,
url,
Expand Down Expand Up @@ -408,7 +415,22 @@ function getPrefetchEntryCacheStatus({
kind,
prefetchTime,
lastUsedTime,
staleTime,
}: PrefetchCacheEntry): PrefetchCacheEntryStatus {
if (staleTime !== -1) {
// `staleTime` is the value sent by the server during static generation.
// When this is available, it takes precedent over any of the heuristics
// that follow.
//
// TODO: When PPR is enabled, the server will *always* return a stale time
// when prefetching. We should never use a prefetch entry that hasn't yet
// received data from the server. So the only two cases should be 1) we use
// the server-generated stale time 2) the unresolved entry is discarded.
return Date.now() < prefetchTime + staleTime
? PrefetchCacheEntryStatus.fresh
: PrefetchCacheEntryStatus.stale
}

// We will re-use the cache entry data for up to the `dynamic` staletime window.
if (Date.now() < (lastUsedTime ?? prefetchTime) + DYNAMIC_STALETIME_MS) {
return lastUsedTime
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ export type PrefetchCacheEntry = {
data: Promise<FetchServerResponseResult>
kind: PrefetchKind
prefetchTime: number
staleTime: number
lastUsedTime: number | null
key: string
status: PrefetchCacheEntryStatus
Expand Down

0 comments on commit 84ab019

Please sign in to comment.