Skip to content

Commit

Permalink
add experimental client router cache config (#62856)
Browse files Browse the repository at this point in the history
This introduces an experimental router flag (`experimental.staleTimes`)
to change the router cache behavior. Specifically:

```ts
// next.config.js
module.exports = {
  experimental: {
    staleTimes: {
      dynamic: <seconds>,
      static: <seconds>,
    },
  },
};
```

- `dynamic` is the value that is used when the `prefetch` `Link` prop is
left unspecified. (Default 30 seconds)
- `static` is the value that is used when the `prefetch` `Link` prop is
`true`. (Default 5 minutes)

Additional details:
- Loading boundaries are considered reusable for the time period
indicated by the `static` property (default 5 minutes)
- This doesn't disable partial rendering support, **meaning shared
layouts won't automatically be refetched every navigation, only the new
segment data**.
- This also doesn't change back/forward caching behavior to ensure
things like scroll restoration etc still work nicely.

Please see the original proposal
[here](#54075 (comment))
for more information. The primary difference is that this is a global
configuration, rather than a per-segment configuration, and it isn't
applied to layouts. (We expect this to be a "stop-gap" and not the final
router caching solution)

Closes NEXT-2703
  • Loading branch information
ztanner committed Apr 2, 2024
1 parent 944159a commit 86cbc1f
Show file tree
Hide file tree
Showing 12 changed files with 461 additions and 86 deletions.
10 changes: 10 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 @@ -171,6 +171,16 @@ 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_DYNAMIC_STALETIME': JSON.stringify(
isNaN(Number(config.experimental.staleTimes?.dynamic))
? 30 // 30 seconds
: config.experimental.staleTimes?.dynamic
),
'process.env.__NEXT_CLIENT_ROUTER_STATIC_STALETIME': JSON.stringify(
isNaN(Number(config.experimental.staleTimes?.static))
? 5 * 60 // 5 minutes
: config.experimental.staleTimes?.static
),
'process.env.__NEXT_CLIENT_ROUTER_FILTER_ENABLED':
config.experimental.clientRouterFilter ?? true,
'process.env.__NEXT_CLIENT_ROUTER_S_FILTER':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,31 +243,38 @@ export function prunePrefetchCache(
}
}

const FIVE_MINUTES = 5 * 60 * 1000
const THIRTY_SECONDS = 30 * 1000
// These values are set by `define-env-plugin` (based on `nextConfig.experimental.staleTimes`)
// and default to 5 minutes (static) / 30 seconds (dynamic)
const DYNAMIC_STALETIME_MS =
Number(process.env.__NEXT_CLIENT_ROUTER_DYNAMIC_STALETIME) * 1000

const STATIC_STALETIME_MS =
Number(process.env.__NEXT_CLIENT_ROUTER_STATIC_STALETIME) * 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) {
// 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
? PrefetchCacheEntryStatus.reusable
: PrefetchCacheEntryStatus.fresh
}

// if the cache entry was prefetched less than 5 mins ago, then we want to re-use only the loading state
// For "auto" prefetching, we'll re-use only the loading boundary for up to `static` staletime window.
// A stale entry will only re-use the `loading` boundary, not the full data.
// This will trigger a "lazy fetch" for the full data.
if (kind === 'auto') {
if (Date.now() < prefetchTime + FIVE_MINUTES) {
if (Date.now() < prefetchTime + STATIC_STALETIME_MS) {
return PrefetchCacheEntryStatus.stale
}
}

// if the cache entry was prefetched less than 5 mins ago and was a "full" prefetch, then we want to re-use it "full
// for "full" prefetching, we'll re-use the cache entry data for up to `static` staletime window.
if (kind === 'full') {
if (Date.now() < prefetchTime + FIVE_MINUTES) {
if (Date.now() < prefetchTime + STATIC_STALETIME_MS) {
return PrefetchCacheEntryStatus.reusable
}
}
Expand Down
6 changes: 6 additions & 0 deletions packages/next/src/server/config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,12 @@ export const configSchema: zod.ZodType<NextConfig> = z.lazy(() =>
validator: z.string().optional(),
})
.optional(),
staleTimes: z
.object({
dynamic: z.number().optional(),
static: z.number().optional(),
})
.optional(),
clientRouterFilter: z.boolean().optional(),
clientRouterFilterRedirects: z.boolean().optional(),
clientRouterFilterAllowedRate: z.number().optional(),
Expand Down
14 changes: 14 additions & 0 deletions packages/next/src/server/config-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,16 @@ export interface ExperimentalConfig {
strictNextHead?: boolean
clientRouterFilter?: boolean
clientRouterFilterRedirects?: boolean
/**
* This config can be used to override the cache behavior for the client router.
* These values indicate the time, in seconds, that the cache should be considered
* reusable. When the `prefetch` Link prop is left unspecified, this will use the `dynamic` value.
* When the `prefetch` Link prop is set to `true`, this will use the `static` value.
*/
staleTimes?: {
dynamic?: number
static?: number
}
// 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 @@ -926,6 +936,10 @@ export const defaultConfig: NextConfig = {
missingSuspenseWithCSRBailout: true,
optimizeServerReact: true,
useEarlyImport: false,
staleTimes: {
dynamic: 30,
static: 300,
},
},
}

Expand Down
10 changes: 10 additions & 0 deletions test/e2e/app-dir/app-client-cache/app/page.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ export default function HomePage() {
To Random Number - prefetch: true
</Link>
</div>
<div>
<Link href="/0?timeout=1000" prefetch={true}>
To Random Number - prefetch: true, slow
</Link>
</div>
<div>
<Link href="/1">To Random Number - prefetch: auto</Link>
</div>
Expand All @@ -15,6 +20,11 @@ export default function HomePage() {
To Random Number 2 - prefetch: false
</Link>
</div>
<div>
<Link href="/2?timeout=1000" prefetch={false}>
To Random Number 2 - prefetch: false, slow
</Link>
</div>
<div>
<Link href="/1?timeout=1000">
To Random Number - prefetch: auto, slow
Expand Down
21 changes: 21 additions & 0 deletions test/e2e/app-dir/app-client-cache/app/without-loading/[id]/page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import Link from 'next/link'

export default async function Page({ searchParams: { timeout } }) {
const randomNumber = await new Promise((resolve) => {
setTimeout(
() => {
resolve(Math.random())
},
timeout !== undefined ? Number.parseInt(timeout, 10) : 0
)
})

return (
<>
<div>
<Link href="/without-loading">Back to Home</Link>
</div>
<div id="random-number">{randomNumber}</div>
</>
)
}
36 changes: 36 additions & 0 deletions test/e2e/app-dir/app-client-cache/app/without-loading/page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import Link from 'next/link'

export default function Page() {
return (
<>
<div>
<Link href="/without-loading/0?timeout=0" prefetch={true}>
To Random Number - prefetch: true
</Link>
</div>
<div>
<Link href="/without-loading/0?timeout=1000" prefetch={true}>
To Random Number - prefetch: true, slow
</Link>
</div>
<div>
<Link href="/without-loading/1">To Random Number - prefetch: auto</Link>
</div>
<div>
<Link href="/without-loading/2" prefetch={false}>
To Random Number 2 - prefetch: false
</Link>
</div>
<div>
<Link href="/without-loading/2?timeout=1000" prefetch={false}>
To Random Number 2 - prefetch: false, slow
</Link>
</div>
<div>
<Link href="/without-loading/1?timeout=1000">
To Random Number - prefetch: auto, slow
</Link>
</div>
</>
)
}
Loading

0 comments on commit 86cbc1f

Please sign in to comment.