Skip to content

Commit 0c756be

Browse files
authored
Update cache handling for app (#43659)
This updates the app directory caching. x-ref: [slack thread ](https://vercel.slack.com/archives/C042LHPJ1NX/p1669231119199339) ## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have a helpful link attached, see [`contributing.md`](https://github.com/vercel/next.js/blob/canary/contributing.md) ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [ ] [e2e](https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs) tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have a helpful link attached, see [`contributing.md`](https://github.com/vercel/next.js/blob/canary/contributing.md) ## Documentation / Examples - [ ] Make sure the linting passes by running `pnpm build && pnpm lint` - [ ] The "examples guidelines" are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md)
1 parent 3a7c35a commit 0c756be

File tree

23 files changed

+655
-96
lines changed

23 files changed

+655
-96
lines changed

packages/next/client/components/app-router-headers.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export const RSC = 'RSC' as const
22
export const NEXT_ROUTER_STATE_TREE = 'Next-Router-State-Tree' as const
33
export const NEXT_ROUTER_PREFETCH = 'Next-Router-Prefetch' as const
4+
export const FETCH_CACHE_HEADER = 'x-vercel-sc-headers' as const
45
export const RSC_VARY_HEADER =
56
`${RSC}, ${NEXT_ROUTER_STATE_TREE}, ${NEXT_ROUTER_PREFETCH}` as const
67

packages/next/client/components/static-generation-async-storage.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ export interface StaticGenerationStore {
77
fetchRevalidate?: number
88
isStaticGeneration?: boolean
99
forceStatic?: boolean
10+
incrementalCache?: import('../../server/lib/incremental-cache').IncrementalCache
11+
pendingRevalidates?: Promise<any>[]
12+
isRevalidate?: boolean
1013
}
1114

1215
export let staticGenerationAsyncStorage:

packages/next/export/worker.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import '../server/node-polyfill-fetch'
1212
import { loadRequireHook } from '../build/webpack/require-hook'
1313

1414
import { extname, join, dirname, sep } from 'path'
15-
import { promises } from 'fs'
15+
import fs, { promises } from 'fs'
1616
import AmpHtmlValidator from 'next/dist/compiled/amphtml-validator'
1717
import { loadComponents } from '../server/load-components'
1818
import { isDynamicRoute } from '../shared/lib/router/utils/is-dynamic'
@@ -32,6 +32,7 @@ import { normalizeAppPath } from '../shared/lib/router/utils/app-paths'
3232
import { REDIRECT_ERROR_CODE } from '../client/components/redirect'
3333
import { DYNAMIC_ERROR_CODE } from '../client/components/hooks-server-context'
3434
import { NOT_FOUND_ERROR_CODE } from '../client/components/not-found'
35+
import { IncrementalCache } from '../server/lib/incremental-cache'
3536

3637
loadRequireHook()
3738

@@ -98,6 +99,7 @@ interface RenderOpts {
9899
domainLocales?: DomainLocale[]
99100
trailingSlash?: boolean
100101
supportsDynamicHTML?: boolean
102+
incrementalCache?: import('../server/lib/incremental-cache').IncrementalCache
101103
}
102104

103105
// expose AsyncLocalStorage on globalThis for react usage
@@ -310,6 +312,31 @@ export default async function exportPage({
310312
// and bail when dynamic dependencies are detected
311313
// only fully static paths are fully generated here
312314
if (isAppDir) {
315+
curRenderOpts.incrementalCache = new IncrementalCache({
316+
dev: false,
317+
requestHeaders: {},
318+
flushToDisk: true,
319+
maxMemoryCacheSize: 50 * 1024 * 1024,
320+
getPrerenderManifest: () => ({
321+
version: 3,
322+
routes: {},
323+
dynamicRoutes: {},
324+
preview: {
325+
previewModeEncryptionKey: '',
326+
previewModeId: '',
327+
previewModeSigningKey: '',
328+
},
329+
notFoundRoutes: [],
330+
}),
331+
fs: {
332+
readFile: (f) => fs.promises.readFile(f, 'utf8'),
333+
readFileSync: (f) => fs.readFileSync(f, 'utf8'),
334+
writeFile: (f, d) => fs.promises.writeFile(f, d, 'utf8'),
335+
mkdir: (dir) => fs.promises.mkdir(dir, { recursive: true }),
336+
stat: (f) => fs.promises.stat(f),
337+
},
338+
serverDistDir: join(distDir, 'server'),
339+
})
313340
const { renderToHTMLOrFlight } =
314341
require('../server/app-render') as typeof import('../server/app-render')
315342

packages/next/server/app-render.tsx

Lines changed: 122 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ function preloadComponent(Component: any, props: any) {
8080
return Component
8181
}
8282

83+
const CACHE_ONE_YEAR = 31536000
8384
const INTERNAL_HEADERS_INSTANCE = Symbol('internal for headers readonly')
8485

8586
function readonlyHeadersError() {
@@ -176,6 +177,8 @@ export type RenderOptsPartial = {
176177
assetPrefix?: string
177178
fontLoaderManifest?: FontLoaderManifest
178179
isBot?: boolean
180+
incrementalCache?: import('./lib/incremental-cache').IncrementalCache
181+
isRevalidate?: boolean
179182
}
180183

181184
export type RenderOpts = LoadComponentsReturnType & RenderOptsPartial
@@ -245,12 +248,115 @@ function patchFetch(ComponentMod: any) {
245248
const originFetch = globalThis.fetch
246249
globalThis.fetch = async (input, init) => {
247250
const staticGenerationStore =
248-
'getStore' in staticGenerationAsyncStorage
251+
('getStore' in staticGenerationAsyncStorage
249252
? staticGenerationAsyncStorage.getStore()
250-
: staticGenerationAsyncStorage
253+
: staticGenerationAsyncStorage) || {}
254+
255+
const {
256+
isStaticGeneration,
257+
fetchRevalidate,
258+
pathname,
259+
incrementalCache,
260+
isRevalidate,
261+
} = (staticGenerationStore || {}) as StaticGenerationStore
262+
263+
let revalidate: number | undefined | boolean
264+
265+
if (typeof init?.next?.revalidate === 'number') {
266+
revalidate = init.next.revalidate
267+
}
268+
if (init?.next?.revalidate === false) {
269+
revalidate = CACHE_ONE_YEAR
270+
}
271+
272+
if (
273+
!staticGenerationStore.fetchRevalidate ||
274+
(typeof revalidate === 'number' &&
275+
revalidate < staticGenerationStore.fetchRevalidate)
276+
) {
277+
staticGenerationStore.fetchRevalidate = revalidate
278+
}
279+
280+
let cacheKey: string | undefined
281+
282+
const doOriginalFetch = async () => {
283+
return originFetch(input, init).then(async (res) => {
284+
if (
285+
incrementalCache &&
286+
cacheKey &&
287+
typeof revalidate === 'number' &&
288+
revalidate > 0
289+
) {
290+
const clonedRes = res.clone()
291+
292+
let base64Body = ''
293+
294+
if (process.env.NEXT_RUNTIME === 'edge') {
295+
let string = ''
296+
new Uint8Array(await clonedRes.arrayBuffer()).forEach((byte) => {
297+
string += String.fromCharCode(byte)
298+
})
299+
base64Body = btoa(string)
300+
} else {
301+
base64Body = Buffer.from(await clonedRes.arrayBuffer()).toString(
302+
'base64'
303+
)
304+
}
305+
306+
await incrementalCache.set(
307+
cacheKey,
308+
{
309+
kind: 'FETCH',
310+
isStale: false,
311+
age: 0,
312+
data: {
313+
headers: Object.fromEntries(clonedRes.headers.entries()),
314+
body: base64Body,
315+
},
316+
revalidate,
317+
},
318+
revalidate,
319+
true
320+
)
321+
}
322+
return res
323+
})
324+
}
251325

252-
const { isStaticGeneration, fetchRevalidate, pathname } =
253-
staticGenerationStore || {}
326+
if (incrementalCache && typeof revalidate === 'number' && revalidate > 0) {
327+
cacheKey = await incrementalCache?.fetchCacheKey(input.toString(), init)
328+
const entry = await incrementalCache.get(cacheKey, true)
329+
330+
if (entry?.value && entry.value.kind === 'FETCH') {
331+
// when stale and is revalidating we wait for fresh data
332+
// so the revalidated entry has the updated data
333+
if (!isRevalidate || !entry.isStale) {
334+
if (entry.isStale) {
335+
if (!staticGenerationStore.pendingRevalidates) {
336+
staticGenerationStore.pendingRevalidates = []
337+
}
338+
staticGenerationStore.pendingRevalidates.push(
339+
doOriginalFetch().catch(console.error)
340+
)
341+
}
342+
343+
const resData = entry.value.data
344+
let decodedBody = ''
345+
346+
// TODO: handle non-text response bodies
347+
if (process.env.NEXT_RUNTIME === 'edge') {
348+
decodedBody = atob(resData.body)
349+
} else {
350+
decodedBody = Buffer.from(resData.body, 'base64').toString()
351+
}
352+
353+
return new Response(decodedBody, {
354+
headers: resData.headers,
355+
status: resData.status,
356+
})
357+
}
358+
}
359+
}
254360

255361
if (staticGenerationStore && isStaticGeneration) {
256362
if (init && typeof init === 'object') {
@@ -292,7 +398,7 @@ function patchFetch(ComponentMod: any) {
292398
if (hasNextConfig) delete init.next
293399
}
294400
}
295-
return originFetch(input, init)
401+
return doOriginalFetch()
296402
}
297403
}
298404

@@ -771,9 +877,7 @@ export async function renderToHTMLOrFlight(
771877
supportsDynamicHTML,
772878
} = renderOpts
773879

774-
if (process.env.NODE_ENV === 'production') {
775-
patchFetch(ComponentMod)
776-
}
880+
patchFetch(ComponentMod)
777881
const generateStaticHTML = supportsDynamicHTML !== true
778882

779883
const staticGenerationAsyncStorage = ComponentMod.staticGenerationAsyncStorage
@@ -1111,7 +1215,7 @@ export async function renderToHTMLOrFlight(
11111215
// otherwise
11121216
if (layoutOrPageMod.dynamic === 'force-static') {
11131217
staticGenerationStore.forceStatic = true
1114-
} else {
1218+
} else if (layoutOrPageMod.dynamic !== 'error') {
11151219
staticGenerationStore.forceStatic = false
11161220
}
11171221
}
@@ -1726,6 +1830,10 @@ export async function renderToHTMLOrFlight(
17261830
}
17271831
const renderResult = new RenderResult(await bodyResult())
17281832

1833+
if (staticGenerationStore.pendingRevalidates) {
1834+
await Promise.all(staticGenerationStore.pendingRevalidates)
1835+
}
1836+
17291837
if (isStaticGeneration) {
17301838
const htmlResult = await streamToBufferedResult(renderResult)
17311839

@@ -1744,6 +1852,9 @@ export async function renderToHTMLOrFlight(
17441852
await generateFlight()
17451853
)
17461854

1855+
if (staticGenerationStore?.forceStatic === false) {
1856+
staticGenerationStore.fetchRevalidate = 0
1857+
}
17471858
;(renderOpts as any).pageData = filteredFlightData
17481859
;(renderOpts as any).revalidate =
17491860
typeof staticGenerationStore?.fetchRevalidate === 'undefined'
@@ -1760,6 +1871,8 @@ export async function renderToHTMLOrFlight(
17601871
isStaticGeneration,
17611872
inUse: true,
17621873
pathname,
1874+
incrementalCache: renderOpts.incrementalCache,
1875+
isRevalidate: renderOpts.isRevalidate,
17631876
}
17641877

17651878
const tryGetPreviewData =

packages/next/server/base-server.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ import {
7878
RSC,
7979
RSC_VARY_HEADER,
8080
FLIGHT_PARAMETERS,
81+
FETCH_CACHE_HEADER,
8182
} from '../client/components/app-router-headers'
8283

8384
export type FindComponentsResult = {
@@ -310,6 +311,10 @@ export default abstract class Server<ServerOptions extends Options = Options> {
310311
res: BaseNextResponse
311312
): void
312313

314+
protected abstract getIncrementalCache(options: {
315+
requestHeaders: Record<string, undefined | string | string[]>
316+
}): import('./lib/incremental-cache').IncrementalCache
317+
313318
protected abstract getResponseCache(options: {
314319
dev: boolean
315320
}): ResponseCacheBase
@@ -1283,11 +1288,22 @@ export default abstract class Server<ServerOptions extends Options = Options> {
12831288
ssgCacheKey =
12841289
ssgCacheKey === '/index' && pathname === '/' ? '/' : ssgCacheKey
12851290
}
1291+
const incrementalCache = this.getIncrementalCache({
1292+
requestHeaders: Object.assign({}, req.headers),
1293+
})
1294+
if (
1295+
this.nextConfig.experimental.fetchCache &&
1296+
(opts.runtime !== 'experimental-edge' ||
1297+
(this.serverOptions as any).webServerConfig)
1298+
) {
1299+
delete req.headers[FETCH_CACHE_HEADER]
1300+
}
1301+
let isRevalidate = false
12861302

12871303
const doRender: () => Promise<ResponseCacheEntry | null> = async () => {
12881304
let pageData: any
12891305
let body: RenderResult | null
1290-
let sprRevalidate: number | false
1306+
let isrRevalidate: number | false
12911307
let isNotFound: boolean | undefined
12921308
let isRedirect: boolean | undefined
12931309

@@ -1312,6 +1328,12 @@ export default abstract class Server<ServerOptions extends Options = Options> {
13121328
const renderOpts: RenderOpts = {
13131329
...components,
13141330
...opts,
1331+
...(isAppPath && this.nextConfig.experimental.fetchCache
1332+
? {
1333+
incrementalCache,
1334+
isRevalidate: this.minimalMode || isRevalidate,
1335+
}
1336+
: {}),
13151337
isDataReq,
13161338
resolvedUrl,
13171339
locale,
@@ -1346,7 +1368,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
13461368
body = renderResult
13471369
// TODO: change this to a different passing mechanism
13481370
pageData = (renderOpts as any).pageData
1349-
sprRevalidate = (renderOpts as any).revalidate
1371+
isrRevalidate = (renderOpts as any).revalidate
13501372
isNotFound = (renderOpts as any).isNotFound
13511373
isRedirect = (renderOpts as any).isRedirect
13521374

@@ -1361,7 +1383,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
13611383
}
13621384
value = { kind: 'PAGE', html: body, pageData }
13631385
}
1364-
return { revalidate: sprRevalidate, value }
1386+
return { revalidate: isrRevalidate, value }
13651387
}
13661388

13671389
const cacheEntry = await this.responseCache.get(
@@ -1371,6 +1393,10 @@ export default abstract class Server<ServerOptions extends Options = Options> {
13711393
const isDynamicPathname = isDynamicRoute(pathname)
13721394
const didRespond = hasResolved || res.sent
13731395

1396+
if (hadCache) {
1397+
isRevalidate = true
1398+
}
1399+
13741400
if (!staticPaths) {
13751401
;({ staticPaths, fallbackMode } = hasStaticPaths
13761402
? await this.getStaticPaths({ pathname })
@@ -1486,6 +1512,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
14861512
}
14871513
},
14881514
{
1515+
incrementalCache,
14891516
isManualRevalidate,
14901517
isPrefetch: req.headers.purpose === 'prefetch',
14911518
}

packages/next/server/config-schema.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,9 @@ const configSchema = {
277277
fallbackNodePolyfills: {
278278
type: 'boolean',
279279
},
280+
fetchCache: {
281+
type: 'boolean',
282+
},
280283
forceSwcTransforms: {
281284
type: 'boolean',
282285
},

packages/next/server/config-shared.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ export interface NextJsWebpackConfig {
7979
}
8080

8181
export interface ExperimentalConfig {
82+
fetchCache?: boolean
8283
allowMiddlewareResponseBody?: boolean
8384
skipMiddlewareUrlNormalize?: boolean
8485
skipTrailingSlashRedirect?: boolean
@@ -565,6 +566,7 @@ export const defaultConfig: NextConfig = {
565566
swcMinify: true,
566567
output: !!process.env.NEXT_PRIVATE_STANDALONE ? 'standalone' : undefined,
567568
experimental: {
569+
fetchCache: false,
568570
middlewarePrefetch: 'flexible',
569571
optimisticClientCache: true,
570572
runtime: undefined,

0 commit comments

Comments
 (0)