From bd3da32434f97fb1094842aba0eee7c795881a97 Mon Sep 17 00:00:00 2001 From: Zack Tanner Date: Mon, 4 Mar 2024 17:50:05 -0800 Subject: [PATCH] add experimental client router cache config --- .../build/webpack/plugins/define-env-plugin.ts | 7 +++++++ .../router-reducer/prefetch-cache-utils.ts | 16 ++++++++++++---- .../router-reducer/router-reducer-types.ts | 8 ++++++++ packages/next/src/client/link.tsx | 11 +++++++++-- packages/next/src/server/config-schema.ts | 1 + packages/next/src/server/config-shared.ts | 8 ++++++++ 6 files changed, 45 insertions(+), 6 deletions(-) diff --git a/packages/next/src/build/webpack/plugins/define-env-plugin.ts b/packages/next/src/build/webpack/plugins/define-env-plugin.ts index abdfd37a2dd189..86e636874cbf7f 100644 --- a/packages/next/src/build/webpack/plugins/define-env-plugin.ts +++ b/packages/next/src/build/webpack/plugins/define-env-plugin.ts @@ -171,6 +171,13 @@ export function getDefineEnv({ 'process.env.__NEXT_MIDDLEWARE_MATCHERS': middlewareMatchers ?? [], 'process.env.__NEXT_MANUAL_CLIENT_BASE_PATH': config.experimental.manualClientBasePath ?? false, + 'process.env.__NEXT_CLIENT_ROUTER_CACHE_STALETIME_MS': JSON.stringify( + config.experimental.clientRouterCache + ? 31556952000 // 1 year (ms) + : config.experimental.clientRouterCache === false + ? 0 + : 30000 // 30 seconds (ms) + ), 'process.env.__NEXT_CLIENT_ROUTER_FILTER_ENABLED': config.experimental.clientRouterFilter ?? true, 'process.env.__NEXT_CLIENT_ROUTER_S_FILTER': diff --git a/packages/next/src/client/components/router-reducer/prefetch-cache-utils.ts b/packages/next/src/client/components/router-reducer/prefetch-cache-utils.ts index 522ccc6aab11ee..f029680864185b 100644 --- a/packages/next/src/client/components/router-reducer/prefetch-cache-utils.ts +++ b/packages/next/src/client/components/router-reducer/prefetch-cache-utils.ts @@ -8,6 +8,7 @@ import { type PrefetchCacheEntry, PrefetchKind, type ReadonlyReducerState, + PREFETCH_STALE_TIME, } from './router-reducer-types' import { prefetchQueue } from './reducers/prefetch-reducer' @@ -113,7 +114,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), }) @@ -244,15 +246,21 @@ export function prunePrefetchCache( } const FIVE_MINUTES = 5 * 60 * 1000 -const THIRTY_SECONDS = 30 * 1000 function getPrefetchEntryCacheStatus({ kind, 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 diff --git a/packages/next/src/client/components/router-reducer/router-reducer-types.ts b/packages/next/src/client/components/router-reducer/router-reducer-types.ts index 3576281b3d32ec..22acff45ff0f20 100644 --- a/packages/next/src/client/components/router-reducer/router-reducer-types.ts +++ b/packages/next/src/client/components/router-reducer/router-reducer-types.ts @@ -282,3 +282,11 @@ export function isThenable(value: any): value is Promise { 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) diff --git a/packages/next/src/client/link.tsx b/packages/next/src/client/link.tsx index dec2bfc7bc0c2c..d37be47e34aa91 100644 --- a/packages/next/src/client/link.tsx +++ b/packages/next/src/client/link.tsx @@ -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 = { @@ -307,7 +310,11 @@ const Link = React.forwardRef( * - 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: { diff --git a/packages/next/src/server/config-schema.ts b/packages/next/src/server/config-schema.ts index 88b04fbd25d20c..a56cbf9e062569 100644 --- a/packages/next/src/server/config-schema.ts +++ b/packages/next/src/server/config-schema.ts @@ -247,6 +247,7 @@ export const configSchema: zod.ZodType = z.lazy(() => validator: z.string().optional(), }) .optional(), + clientRouterCache: z.boolean().optional(), clientRouterFilter: z.boolean().optional(), clientRouterFilterRedirects: z.boolean().optional(), clientRouterFilterAllowedRate: z.number().optional(), diff --git a/packages/next/src/server/config-shared.ts b/packages/next/src/server/config-shared.ts index d9c7a176efa438..3d49c0202a8cf0 100644 --- a/packages/next/src/server/config-shared.ts +++ b/packages/next/src/server/config-shared.ts @@ -187,6 +187,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 @@ -926,6 +933,7 @@ export const defaultConfig: NextConfig = { missingSuspenseWithCSRBailout: true, optimizeServerReact: true, useEarlyImport: false, + clientRouterCache: undefined, }, }