Skip to content

Commit

Permalink
Stabilize custom cache handlers and changing memory size. (#57953)
Browse files Browse the repository at this point in the history
This PR stabilizes the previously introduced experimental config options
for providing a custom cache handler (for both ISR as well as the Data
Cache) and for disabling or configuring the in-memory cache size. The
example usage would be as follows:

```js
// next.config.js
module.exports = {
  cacheHandler: require.resolve('./cache-handler.js'),
  cacheMaxMemorySize: 0 // disable default in-memory caching
}
```

This PR also updates the documentation to better reflect how to use the
custom cache handler when self-hosting. Further information will be
added in a following PR that also includes a full example of a custom
cache handler that implements `revalidateTag` as well as passing in
custom cache tags. The API reference docs have been updated here, as
well as a version history added.

I also noticed that we currently have two duplicated versions of the ISR
docs in the Pages Router docs: both for rendering and for data fetching.
Data Fetching is the correct location for this page. There were no other
references to the rendering version in the docs, so that must have been
an accident. I'll need to a get a redirect going for that regardless.

Tests have been updated for `cacheHandler` and I added a new test for
`cacheMaxMemorySize`.

---------

Co-authored-by: Jiachi Liu <inbox@huozhi.im>
  • Loading branch information
leerob and huozhi committed Jan 17, 2024
1 parent 8e216ec commit 16dcebe
Show file tree
Hide file tree
Showing 23 changed files with 119 additions and 144 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -146,10 +146,8 @@ To configure the ISR/Data Cache location when self-hosting, you can configure a

```jsx filename="next.config.js"
module.exports = {
experimental: {
incrementalCacheHandlerPath: require.resolve('./cache-handler.js'),
isrMemoryCacheSize: 0, // disable default in-memory caching
},
cacheHandler: require.resolve('./cache-handler.js'),
cacheMaxMemorySize: 0, // disable default in-memory caching
}
```

Expand Down
Original file line number Diff line number Diff line change
@@ -1,41 +1,19 @@
---
title: incrementalCacheHandlerPath
description: Configure the Next.js cache used for storing and revalidating data.
title: Custom Next.js Cache Handler
nav_title: cacheHandler
description: Configure the Next.js cache used for storing and revalidating data to use any external service like Redis, Memcached, or others.
---

In Next.js, the [default cache handler](/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating) uses the filesystem cache. This requires no configuration, however, you can customize the cache handler by using the `incrementalCacheHandlerPath` field in `next.config.js`.
In Next.js, the [default cache handler](/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating) for the Pages and App Router uses the filesystem cache. This requires no configuration, however, you can customize the cache handler by using the `cacheHandler` field in `next.config.js`.

```js filename="next.config.js"
module.exports = {
experimental: {
incrementalCacheHandlerPath: require.resolve('./cache-handler.js'),
},
cacheHandler: require.resolve('./cache-handler.js'),
cacheMaxMemorySize: 0, // disable default in-memory caching
}
```

Here's an example of a custom cache handler:

```js filename="cache-handler.js"
const cache = new Map()

module.exports = class CacheHandler {
constructor(options) {
this.options = options
this.cache = {}
}

async get(key) {
return cache.get(key)
}

async set(key, data) {
cache.set(key, {
value: data,
lastModified: Date.now(),
})
}
}
```
View an example of a [custom cache handler](/docs/app/building-your-application/deploying#configuring-caching) and learn more about implementation.

## API Reference

Expand All @@ -55,6 +33,7 @@ Returns the cached value or `null` if not found.
| --------- | -------------- | -------------------------------- |
| `key` | `string` | The key to store the data under. |
| `data` | Data or `null` | The data to be cached. |
| `ctx` | `{ tags: [] }` | The cache tags provided. |

Returns `Promise<void>`.

Expand All @@ -65,3 +44,16 @@ Returns `Promise<void>`.
| `tag` | `string` | The cache tag to revalidate. |

Returns `Promise<void>`. Learn more about [revalidating data](/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating) or the [`revalidateTag()`](/docs/app/api-reference/functions/revalidateTag) function.

**Good to know:**

- `revalidatePath` is a convenience layer on top of cache tags. Calling `revalidatePath` will call your `revalidateTag` function, which you can then choose if you want to tag cache keys based on the path.

## Version History

| Version | Changes |
| --------- | ------------------------------------------------------------------------ |
| `v14.1.0` | Renamed `cacheHandler` is stable. |
| `v13.4.0` | `incrementalCacheHandlerPath` (experimental) supports `revalidateTag`. |
| `v13.4.0` | `incrementalCacheHandlerPath` (experimental) supports standalone output. |
| `v12.2.0` | `incrementalCacheHandlerPath` (experimental) is added. |
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Incremental Static Regeneration
description: 'Learn how to create or update static pages at runtime with Incremental Static Regeneration.'
title: Incremental Static Regeneration (ISR)
description: Learn how to create or update static pages at runtime with Incremental Static Regeneration.
---

<details>
Expand Down Expand Up @@ -169,29 +169,13 @@ export async function getStaticProps() {

Incremental Static Regeneration (ISR) works on [self-hosted Next.js sites](/docs/pages/building-your-application/deploying#self-hosting) out of the box when you use `next start`.

You can use this approach when deploying to container orchestrators such as [Kubernetes](https://kubernetes.io/) or [HashiCorp Nomad](https://www.nomadproject.io/). By default, generated assets will be stored in-memory on each pod. This means that each pod will have its own copy of the static files. Stale data may be shown until that specific pod is hit by a request.

To ensure consistency across all pods, you can disable in-memory caching. This will inform the Next.js server to only leverage assets generated by ISR in the file system.

You can use a shared network mount in your Kubernetes pods (or similar setup) to reuse the same file-system cache between different containers. By sharing the same mount, the `.next` folder which contains the `next/image` cache will also be shared and re-used.

To disable in-memory caching, set `isrMemoryCacheSize` to `0` in your `next.config.js` file:

```js filename="next.config.js"
module.exports = {
experimental: {
// Defaults to 50MB
isrMemoryCacheSize: 0, // cache size in bytes
},
}
```

> **Good to know**: You might need to consider a race condition between multiple pods trying to update the cache at the same time, depending on how your shared mount is configured.
Learn more about [self-hosting Next.js](/docs/pages/building-your-application/deploying#self-hosting).

## Version History

| Version | Changes |
| --------- | --------------------------------------------------------------------------------------- |
| `v14.1.0` | Custom `cacheHandler` is stable. |
| `v12.2.0` | On-Demand ISR is stable |
| `v12.1.0` | On-Demand ISR added (beta). |
| `v12.0.0` | [Bot-aware ISR fallback](https://nextjs.org/blog/next-12#bot-aware-isr-fallback) added. |
Expand Down
10 changes: 5 additions & 5 deletions packages/next/src/build/collect-build-traces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,16 +239,16 @@ export async function collectBuildTraces({
)),
]

const { incrementalCacheHandlerPath } = config.experimental
const { cacheHandler } = config

// ensure we trace any dependencies needed for custom
// incremental cache handler
if (incrementalCacheHandlerPath) {
if (cacheHandler) {
sharedEntriesSet.push(
require.resolve(
path.isAbsolute(incrementalCacheHandlerPath)
? incrementalCacheHandlerPath
: path.join(dir, incrementalCacheHandlerPath)
path.isAbsolute(cacheHandler)
? cacheHandler
: path.join(dir, cacheHandler)
)
)
}
Expand Down
3 changes: 1 addition & 2 deletions packages/next/src/build/entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -414,8 +414,7 @@ export function getEdgeServerEntry(opts: {
pagesType: opts.pagesType,
appDirLoader: Buffer.from(opts.appDirLoader || '').toString('base64'),
sriEnabled: !opts.isDev && !!opts.config.experimental.sri?.algorithm,
incrementalCacheHandlerPath:
opts.config.experimental.incrementalCacheHandlerPath,
cacheHandler: opts.config.cacheHandler,
preferredRegion: opts.preferredRegion,
middlewareConfig: Buffer.from(
JSON.stringify(opts.middlewareConfig || {})
Expand Down
24 changes: 11 additions & 13 deletions packages/next/src/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1245,7 +1245,7 @@ export default async function build(
PAGES_MANIFEST
)

const { incrementalCacheHandlerPath } = config.experimental
const { cacheHandler } = config

const requiredServerFilesManifest = nextBuildSpan
.traceChild('generate-required-server-files')
Expand All @@ -1260,12 +1260,12 @@ export default async function build(
compress: false,
}
: {}),
cacheHandler: cacheHandler
? path.relative(distDir, cacheHandler)
: config.cacheHandler,
experimental: {
...config.experimental,
trustHostHeader: ciEnvironment.hasNextSupport,
incrementalCacheHandlerPath: incrementalCacheHandlerPath
? path.relative(distDir, incrementalCacheHandlerPath)
: undefined,

// @ts-expect-error internal field TODO: fix this, should use a separate mechanism to pass the info.
isExperimentalCompile: isCompileMode,
Expand Down Expand Up @@ -1523,11 +1523,11 @@ export default async function build(

if (config.experimental.staticWorkerRequestDeduping) {
let CacheHandler
if (incrementalCacheHandlerPath) {
if (cacheHandler) {
CacheHandler = interopDefault(
await import(
formatDynamicImportPath(dir, incrementalCacheHandlerPath)
).then((mod) => mod.default || mod)
await import(formatDynamicImportPath(dir, cacheHandler)).then(
(mod) => mod.default || mod
)
)
}

Expand All @@ -1542,7 +1542,7 @@ export default async function build(
: config.experimental.isrFlushToDisk,
serverDistDir: path.join(distDir, 'server'),
fetchCacheKeyPrefix: config.experimental.fetchCacheKeyPrefix,
maxMemoryCacheSize: config.experimental.isrMemoryCacheSize,
maxMemoryCacheSize: config.cacheMaxMemorySize,
getPrerenderManifest: () => ({
version: -1 as any, // letting us know this doesn't conform to spec
routes: {},
Expand Down Expand Up @@ -1840,13 +1840,11 @@ export default async function build(
pageRuntime,
edgeInfo,
pageType,
incrementalCacheHandlerPath:
config.experimental.incrementalCacheHandlerPath,
cacheHandler: config.cacheHandler,
isrFlushToDisk: ciEnvironment.hasNextSupport
? false
: config.experimental.isrFlushToDisk,
maxMemoryCacheSize:
config.experimental.isrMemoryCacheSize,
maxMemoryCacheSize: config.cacheMaxMemorySize,
nextConfigOutput: config.output,
ppr: config.experimental.ppr === true,
})
Expand Down
18 changes: 9 additions & 9 deletions packages/next/src/build/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1304,7 +1304,7 @@ export async function buildAppStaticPaths({
configFileName,
generateParams,
isrFlushToDisk,
incrementalCacheHandlerPath,
cacheHandler,
requestHeaders,
maxMemoryCacheSize,
fetchCacheKeyPrefix,
Expand All @@ -1315,10 +1315,10 @@ export async function buildAppStaticPaths({
page: string
configFileName: string
generateParams: GenerateParamsResults
incrementalCacheHandlerPath?: string
distDir: string
isrFlushToDisk?: boolean
fetchCacheKeyPrefix?: string
cacheHandler?: string
maxMemoryCacheSize?: number
requestHeaders: IncrementalCache['requestHeaders']
ppr: boolean
Expand All @@ -1328,11 +1328,11 @@ export async function buildAppStaticPaths({

let CacheHandler: any

if (incrementalCacheHandlerPath) {
if (cacheHandler) {
CacheHandler = interopDefault(
await import(
formatDynamicImportPath(dir, incrementalCacheHandlerPath)
).then((mod) => mod.default || mod)
await import(formatDynamicImportPath(dir, cacheHandler)).then(
(mod) => mod.default || mod
)
)
}

Expand Down Expand Up @@ -1483,7 +1483,7 @@ export async function isPageStatic({
originalAppPath,
isrFlushToDisk,
maxMemoryCacheSize,
incrementalCacheHandlerPath,
cacheHandler,
ppr,
}: {
dir: string
Expand All @@ -1501,7 +1501,7 @@ export async function isPageStatic({
originalAppPath?: string
isrFlushToDisk?: boolean
maxMemoryCacheSize?: number
incrementalCacheHandlerPath?: string
cacheHandler?: string
nextConfigOutput: 'standalone' | 'export'
ppr: boolean
}): Promise<{
Expand Down Expand Up @@ -1667,7 +1667,7 @@ export async function isPageStatic({
requestHeaders: {},
isrFlushToDisk,
maxMemoryCacheSize,
incrementalCacheHandlerPath,
cacheHandler,
ppr,
ComponentMod,
}))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export type EdgeSSRLoaderQuery = {
appDirLoader?: string
pagesType: PAGE_TYPES
sriEnabled: boolean
incrementalCacheHandlerPath?: string
cacheHandler?: string
preferredRegion: string | string[] | undefined
middlewareConfig: string
serverActions?: {
Expand Down Expand Up @@ -76,7 +76,7 @@ const edgeSSRLoader: webpack.LoaderDefinitionFunction<EdgeSSRLoaderQuery> =
appDirLoader: appDirLoaderBase64,
pagesType,
sriEnabled,
incrementalCacheHandlerPath,
cacheHandler,
preferredRegion,
middlewareConfig: middlewareConfigBase64,
serverActions,
Expand Down Expand Up @@ -158,7 +158,7 @@ const edgeSSRLoader: webpack.LoaderDefinitionFunction<EdgeSSRLoaderQuery> =
: JSON.stringify(serverActions),
},
{
incrementalCacheHandler: incrementalCacheHandlerPath ?? null,
incrementalCacheHandler: cacheHandler ?? null,
}
)
} else {
Expand Down Expand Up @@ -187,7 +187,7 @@ const edgeSSRLoader: webpack.LoaderDefinitionFunction<EdgeSSRLoaderQuery> =
},
{
userland500Page: userland500Path,
incrementalCacheHandler: incrementalCacheHandlerPath ?? null,
incrementalCacheHandler: cacheHandler ?? null,
}
)
}
Expand Down
18 changes: 9 additions & 9 deletions packages/next/src/export/helpers/create-incremental-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@ import { interopDefault } from '../../lib/interop-default'
import { formatDynamicImportPath } from '../../lib/format-dynamic-import-path'

export async function createIncrementalCache({
incrementalCacheHandlerPath,
isrMemoryCacheSize,
cacheHandler,
cacheMaxMemorySize,
fetchCacheKeyPrefix,
distDir,
dir,
enabledDirectories,
experimental,
flushToDisk,
}: {
incrementalCacheHandlerPath?: string
isrMemoryCacheSize?: number
cacheHandler?: string
cacheMaxMemorySize?: number
fetchCacheKeyPrefix?: string
distDir: string
dir: string
Expand All @@ -28,11 +28,11 @@ export async function createIncrementalCache({
}) {
// Custom cache handler overrides.
let CacheHandler: any
if (incrementalCacheHandlerPath) {
if (cacheHandler) {
CacheHandler = interopDefault(
await import(
formatDynamicImportPath(dir, incrementalCacheHandlerPath)
).then((mod) => mod.default || mod)
await import(formatDynamicImportPath(dir, cacheHandler)).then(
(mod) => mod.default || mod
)
)
}

Expand All @@ -41,7 +41,7 @@ export async function createIncrementalCache({
requestHeaders: {},
flushToDisk,
fetchCache: true,
maxMemoryCacheSize: isrMemoryCacheSize,
maxMemoryCacheSize: cacheMaxMemorySize,
fetchCacheKeyPrefix,
getPrerenderManifest: () => ({
version: 4,
Expand Down
5 changes: 2 additions & 3 deletions packages/next/src/export/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -701,11 +701,10 @@ export async function exportAppImpl(
parentSpanId: pageExportSpan.getId(),
httpAgentOptions: nextConfig.httpAgentOptions,
debugOutput: options.debugOutput,
isrMemoryCacheSize: nextConfig.experimental.isrMemoryCacheSize,
cacheMaxMemorySize: nextConfig.cacheMaxMemorySize,
fetchCache: true,
fetchCacheKeyPrefix: nextConfig.experimental.fetchCacheKeyPrefix,
incrementalCacheHandlerPath:
nextConfig.experimental.incrementalCacheHandlerPath,
cacheHandler: nextConfig.cacheHandler,
enableExperimentalReact: needsExperimentalReact(nextConfig),
enabledDirectories,
})
Expand Down
4 changes: 2 additions & 2 deletions packages/next/src/export/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@ export interface ExportPageInput {
parentSpanId: any
httpAgentOptions: NextConfigComplete['httpAgentOptions']
debugOutput?: boolean
isrMemoryCacheSize?: NextConfigComplete['experimental']['isrMemoryCacheSize']
cacheMaxMemorySize?: NextConfigComplete['cacheMaxMemorySize']
fetchCache?: boolean
incrementalCacheHandlerPath?: string
cacheHandler?: string
fetchCacheKeyPrefix?: string
nextConfigOutput?: NextConfigComplete['output']
enableExperimentalReact?: boolean
Expand Down
Loading

0 comments on commit 16dcebe

Please sign in to comment.