Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stabilize custom cache handlers and changing memory size. #57953

Merged
merged 11 commits into from
Jan 17, 2024
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)
// : undefined,
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,8 +1304,8 @@ export async function buildAppStaticPaths({
configFileName,
generateParams,
isrFlushToDisk,
incrementalCacheHandlerPath,
requestHeaders,
cacheHandler,
maxMemoryCacheSize,
fetchCacheKeyPrefix,
ppr,
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
Loading