Skip to content

Commit

Permalink
add experimental client router cache config
Browse files Browse the repository at this point in the history
  • Loading branch information
ztanner committed Mar 5, 2024
1 parent ff5bc7d commit 98f5d0d
Show file tree
Hide file tree
Showing 5 changed files with 55 additions and 8 deletions.
7 changes: 7 additions & 0 deletions packages/next/src/build/webpack/plugins/define-env-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,13 @@ export function getDefineEnv({
'process.env.__NEXT_CLIENT_ROUTER_D_FILTER': JSON.stringify(
clientRouterFilters?.dynamicFilter
),
'process.env.__NEXT_CLIENT_ROUTER_CACHE_STALETIME_MS': JSON.stringify(
config.experimental.clientRouterCache
? 31556952000 // 1 year in milliseconds
: config.experimental.clientRouterCache === false
? 0
: 30 * 1000 // 30 seconds
),
'process.env.__NEXT_OPTIMISTIC_CLIENT_CACHE': JSON.stringify(
config.experimental.optimisticClientCache
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
type PrefetchCacheEntry,
PrefetchKind,
type ReadonlyReducerState,
PREFETCH_STALE_TIME,
} from './router-reducer-types'
import { prefetchQueue } from './reducers/prefetch-reducer'

Expand Down Expand Up @@ -82,11 +83,20 @@ export function getOrCreatePrefetchCacheEntry({
existingCacheEntry.kind !== PrefetchKind.FULL &&
kind === PrefetchKind.FULL

// If the cache entry isn't reusable, rather than returning it, we want to create a new entry.
const hasReusablePrefetch =
const hasReusableLoadingState =
// If staletime is 0, we'd be throwing away the prefetch entry every navigation.
// This means we'd never get a chance to re-use the previous loading state, de-opting out of instant navigations.
PREFETCH_STALE_TIME === 0 &&
(existingCacheEntry.loadingStatus === PrefetchCacheEntryStatus.fresh ||
existingCacheEntry.loadingStatus === PrefetchCacheEntryStatus.reusable)
const hasReusableData =
existingCacheEntry.status === PrefetchCacheEntryStatus.reusable ||
existingCacheEntry.status === PrefetchCacheEntryStatus.fresh

// we'll let the router use the existing prefetch entry if anything can be reused (loading state, or the data itself)
// otherwise we will fetch fresh data from the server and update the cache entry
const hasReusablePrefetch = hasReusableLoadingState || hasReusableData

if (switchedToFullPrefetch || !hasReusablePrefetch) {
return createLazyPrefetchEntry({
tree,
Expand Down Expand Up @@ -122,7 +132,8 @@ export function getOrCreatePrefetchCacheEntry({
kind:
kind ||
// in dev, there's never gonna be a prefetch entry so we want to prefetch here
(process.env.NODE_ENV === 'development'
// when staletime is 0, there'll never be a "FULL" prefetch kind, so we default to auto
(process.env.NODE_ENV === 'development' || PREFETCH_STALE_TIME === 0
? PrefetchKind.AUTO
: PrefetchKind.TEMPORARY),
})
Expand Down Expand Up @@ -259,7 +270,6 @@ export function prunePrefetchCache(
}

const FIVE_MINUTES = 5 * 60 * 1000
const THIRTY_SECONDS = 30 * 1000

/**
* This function is used to determine the cache status of the loading state of a prefetch cache entry.
Expand Down Expand Up @@ -287,8 +297,15 @@ function getPrefetchEntryCacheStatus({
prefetchTime,
lastUsedTime,
}: PrefetchCacheEntry): PrefetchCacheEntryStatus {
// if the cache entry was prefetched or read less than 30s ago, then we want to re-use it
if (Date.now() < (lastUsedTime ?? prefetchTime) + THIRTY_SECONDS) {
if (PREFETCH_STALE_TIME === 0) {
// a value of 0 means we never want to use the prefetch data, only the prefetched loading state (if it exists)
// we mark it stale here so that the router will not attempt to apply the cache node data and will instead know to lazily
// fetch the full data
return PrefetchCacheEntryStatus.stale
}

// if the cache entry was prefetched or read less than the specified staletime window, then we want to re-use it
if (Date.now() < (lastUsedTime ?? prefetchTime) + PREFETCH_STALE_TIME) {
return lastUsedTime
? PrefetchCacheEntryStatus.reusable
: PrefetchCacheEntryStatus.fresh
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -284,3 +284,11 @@ export function isThenable(value: any): value is Promise<AppRouterState> {
typeof value.then === 'function'
)
}

/**
* Time (in ms) that a prefetch entry can be reused by the client router cache.
*/
export const PREFETCH_STALE_TIME =
typeof process.env.__NEXT_CLIENT_ROUTER_CACHE_STALETIME_MS !== 'undefined'
? parseInt(process.env.__NEXT_CLIENT_ROUTER_CACHE_STALETIME_MS, 10)
: 30 * 1000 // thirty seconds (in ms)
11 changes: 9 additions & 2 deletions packages/next/src/client/link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ import type {
import { useIntersection } from './use-intersection'
import { getDomainLocale } from './get-domain-locale'
import { addBasePath } from './add-base-path'
import { PrefetchKind } from './components/router-reducer/router-reducer-types'
import {
PREFETCH_STALE_TIME,
PrefetchKind,
} from './components/router-reducer/router-reducer-types'

type Url = string | UrlObject
type RequiredKeys<T> = {
Expand Down Expand Up @@ -307,7 +310,11 @@ const Link = React.forwardRef<HTMLAnchorElement, LinkPropsReal>(
* - false: we will not prefetch if in the viewport at all
*/
const appPrefetchKind =
prefetchProp === null ? PrefetchKind.AUTO : PrefetchKind.FULL
// If the prefetch staletime is 0, then a full prefetch would be wasteful, as it'd never get used.
// These get switched into "auto" so at least the loading state can be re-used.
prefetchProp === null || PREFETCH_STALE_TIME === 0
? PrefetchKind.AUTO
: PrefetchKind.FULL

if (process.env.NODE_ENV !== 'production') {
function createPropError(args: {
Expand Down
8 changes: 8 additions & 0 deletions packages/next/src/server/config-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,13 @@ export interface ExperimentalConfig {
strictNextHead?: boolean
clientRouterFilter?: boolean
clientRouterFilterRedirects?: boolean
/**
* Used to control the router cache "stale time" value. This value is used to determine
* if a cache entry can be re-used, and for how long. `true` means "forever" (implemented as
* one year), `false` means "never" (will always fetch from the server), and undefined will use
* the existing heuristics (30s for dynamic routes, 5min for static routes)
*/
clientRouterCache?: boolean
// decimal for percent for possible false positives
// e.g. 0.01 for 10% potential false matches lower
// percent increases size of the filter
Expand Down Expand Up @@ -908,6 +915,7 @@ export const defaultConfig: NextConfig = {
optimizeServerReact: false,
useEarlyImport: false,
mergeCssChunks: true,
clientRouterCache: undefined,
},
}

Expand Down

0 comments on commit 98f5d0d

Please sign in to comment.