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 1794f59 commit e291268
Show file tree
Hide file tree
Showing 6 changed files with 42 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ describe('createInitialRouterState', () => {
treeAtTimeOfPrefetch: initialTree,
status: PrefetchCacheEntryStatus.fresh,
url: new URL('/linking', 'https://localhost'),
staleTime: -1,
},
],
]),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,13 @@ export function createInitialRouterState({
couldBeIntercepted: !!couldBeIntercepted,
prerendered,
postponed,
// TODO: The initial RSC payload includes both static and dynamic data
// in the same response, even if PPR is enabled. So if there's any
// dynamic data at all, we can't set a stale time. In the future we may
// add a way to split a single Flight stream into static and dynamic
// parts. But in the meantime we should at least make this work for
// fully static pages.
staleTime: -1,
},
tree: initialState.tree,
prefetchCache: initialState.prefetchCache,
Expand Down
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 precedence 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 @@ -350,6 +350,9 @@ export function serverActionReducer(
couldBeIntercepted: false,
prerendered: false,
postponed: false,
// TODO: We should be able to set this if the server action
// returned a fully static response.
staleTime: -1,
},
tree: state.tree,
prefetchCache: state.prefetchCache,
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 e291268

Please sign in to comment.