Skip to content

Commit 2c95fef

Browse files
authored
[dynamicIO] Use filled Resume Data Cache for final-phase prerenders (#79743)
When a route with dynamic params is prerendered using defined params from `generateStaticParams`, the filled Resume Data Cache is now used for the final-phase prerender of the matching optional fallback shell. This ensures that the fallback shell can reuse existing cache entries, which partially mitigates the performance hit we accepted by splitting up the prerendering into two phases. Furthermore, it will allow us to short-circuit fallback params access in `"use cache"` functions. Instead of having to wait for the timeout error, we will be able to regard cache misses (excluding cached pages and layouts, whose params access is already handled swiftly) to be caused by including the params in the cache key, if `allowEmptyFallbackShell` is `true`.
1 parent c6775ae commit 2c95fef

File tree

17 files changed

+272
-71
lines changed

17 files changed

+272
-71
lines changed

packages/next/src/build/templates/app-page.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -365,9 +365,9 @@ export async function handler(
365365

366366
// If the warmup is successful, we should use the resume data
367367
// cache from the warmup.
368-
if (warmup.metadata.devRenderResumeDataCache) {
369-
context.renderOpts.devRenderResumeDataCache =
370-
warmup.metadata.devRenderResumeDataCache
368+
if (warmup.metadata.renderResumeDataCache) {
369+
context.renderOpts.renderResumeDataCache =
370+
warmup.metadata.renderResumeDataCache
371371
}
372372
}
373373
}

packages/next/src/export/index.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -560,7 +560,8 @@ async function exportAppImpl(
560560

561561
const exportPagesInBatches = async (
562562
worker: StaticWorker,
563-
exportPaths: ExportPathEntry[]
563+
exportPaths: ExportPathEntry[],
564+
renderResumeDataCachesByPage?: Record<string, string>
564565
): Promise<ExportPagesResult> => {
565566
// Batch filtered pages into smaller batches, and call the export worker on
566567
// each batch. We've set a default minimum of 25 pages per batch to ensure
@@ -607,6 +608,7 @@ async function exportAppImpl(
607608
cacheMaxMemorySize: nextConfig.cacheMaxMemorySize,
608609
fetchCache: true,
609610
fetchCacheKeyPrefix: nextConfig.experimental.fetchCacheKeyPrefix,
611+
renderResumeDataCachesByPage,
610612
})
611613
)
612614
)
@@ -641,7 +643,31 @@ async function exportAppImpl(
641643
const results = await exportPagesInBatches(worker, initialPhaseExportPaths)
642644

643645
if (finalPhaseExportPaths.length > 0) {
644-
results.push(...(await exportPagesInBatches(worker, finalPhaseExportPaths)))
646+
const renderResumeDataCachesByPage: Record<string, string> = {}
647+
648+
for (const { page, result } of results) {
649+
if (!result) {
650+
continue
651+
}
652+
653+
if ('renderResumeDataCache' in result && result.renderResumeDataCache) {
654+
// The last RDC for each page is used. We only need one. It should have
655+
// all the entries that the fallback shell also needs. We don't need to
656+
// merge them per page.
657+
renderResumeDataCachesByPage[page] = result.renderResumeDataCache
658+
// Remove the RDC string from the result so that it can be garbage
659+
// collected, when there are more results for the same page.
660+
result.renderResumeDataCache = undefined
661+
}
662+
}
663+
664+
const finalPhaseResults = await exportPagesInBatches(
665+
worker,
666+
finalPhaseExportPaths,
667+
renderResumeDataCachesByPage
668+
)
669+
670+
results.push(...finalPhaseResults)
645671
}
646672

647673
let hadValidationError = false

packages/next/src/export/routes/app-page.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { AfterRunner } from '../../server/after/run-with-after'
2929
import type { RequestLifecycleOpts } from '../../server/base-server'
3030
import type { AppSharedContext } from '../../server/app-render/app-render'
3131
import type { MultiFileWriter } from '../../lib/multi-file-writer'
32+
import { stringifyResumeDataCache } from '../../server/resume-data-cache/resume-data-cache'
3233

3334
/**
3435
* Renders & exports a page associated with the /app directory
@@ -92,6 +93,7 @@ export async function exportAppPage(
9293
fetchTags,
9394
fetchMetrics,
9495
segmentData,
96+
renderResumeDataCache,
9597
} = metadata
9698

9799
// Ensure we don't postpone without having PPR enabled.
@@ -220,6 +222,9 @@ export async function exportAppPage(
220222
hasPostponed: Boolean(postponed),
221223
cacheControl,
222224
fetchMetrics,
225+
renderResumeDataCache: renderResumeDataCache
226+
? await stringifyResumeDataCache(renderResumeDataCache)
227+
: undefined,
223228
}
224229
} catch (err) {
225230
if (!isDynamicUsageError(err)) {

packages/next/src/export/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type {
1212
} from '../build/turborepo-access-trace'
1313
import type { FetchMetrics } from '../server/base-http'
1414
import type { RouteMetadata } from './routes/types'
15+
import type { RenderResumeDataCache } from '../server/resume-data-cache/resume-data-cache'
1516

1617
export interface AmpValidation {
1718
page: string
@@ -40,6 +41,7 @@ export interface ExportPagesInput {
4041
cacheHandler: string | undefined
4142
fetchCacheKeyPrefix: string | undefined
4243
options: ExportAppOptions
44+
renderResumeDataCachesByPage: Record<string, string> | undefined
4345
}
4446

4547
export interface ExportPageInput {
@@ -62,6 +64,7 @@ export interface ExportPageInput {
6264
nextConfigOutput?: NextConfigComplete['output']
6365
enableExperimentalReact?: boolean
6466
sriEnabled: boolean
67+
renderResumeDataCache: RenderResumeDataCache | undefined
6568
}
6669

6770
export type ExportRouteResult =
@@ -73,6 +76,7 @@ export type ExportRouteResult =
7376
hasEmptyStaticShell?: boolean
7477
hasPostponed?: boolean
7578
fetchMetrics?: FetchMetrics
79+
renderResumeDataCache?: string
7680
}
7781
| {
7882
error: boolean

packages/next/src/export/worker.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import { isStaticGenBailoutError } from '../client/components/static-generation-
4949
import type { PagesRenderContext, PagesSharedContext } from '../server/render'
5050
import type { AppSharedContext } from '../server/app-render/app-render'
5151
import { MultiFileWriter } from '../lib/multi-file-writer'
52+
import { createRenderResumeDataCache } from '../server/resume-data-cache/resume-data-cache'
5253

5354
const envConfig =
5455
require('../shared/lib/runtime-config.external') as typeof import('../shared/lib/runtime-config.external')
@@ -86,6 +87,7 @@ async function exportPageImpl(
8687
renderOpts: commonRenderOpts,
8788
outDir: commonOutDir,
8889
buildId,
90+
renderResumeDataCache,
8991
} = input
9092

9193
if (enableExperimentalReact) {
@@ -276,6 +278,7 @@ async function exportPageImpl(
276278
...commonRenderOpts.experimental,
277279
isRoutePPREnabled,
278280
},
281+
renderResumeDataCache,
279282
}
280283

281284
if (hasNextSupport) {
@@ -355,6 +358,7 @@ export async function exportPages(
355358
renderOpts,
356359
nextConfig,
357360
options,
361+
renderResumeDataCachesByPage = {},
358362
} = input
359363

360364
if (nextConfig.experimental.enablePrerenderSourceMaps) {
@@ -399,6 +403,10 @@ export async function exportPages(
399403
// Also tests for `inspect-brk`
400404
process.env.NODE_OPTIONS?.includes('--inspect')
401405

406+
const renderResumeDataCache = renderResumeDataCachesByPage[page]
407+
? createRenderResumeDataCache(renderResumeDataCachesByPage[page])
408+
: undefined
409+
402410
while (attempt < maxAttempts) {
403411
try {
404412
result = await Promise.race<ExportPageResult | undefined>([
@@ -423,6 +431,7 @@ export async function exportPages(
423431
enableExperimentalReact: needsExperimentalReact(nextConfig),
424432
sriEnabled: Boolean(nextConfig.experimental.sri?.algorithm),
425433
buildId: input.buildId,
434+
renderResumeDataCache,
426435
}),
427436
hasDebuggerAttached
428437
? // With a debugger attached, exporting can take infinitely if we paused script execution.
@@ -591,15 +600,9 @@ async function exportPage(
591600

592601
// Otherwise we can return the result.
593602
return {
603+
...result,
594604
duration: Date.now() - start,
595-
ampValidations: result.ampValidations,
596-
cacheControl: result.cacheControl,
597-
metadata: result.metadata,
598-
ssgNotFound: result.ssgNotFound,
599-
hasEmptyStaticShell: result.hasEmptyStaticShell,
600-
hasPostponed: result.hasPostponed,
601605
turborepoAccessTraceResult: turborepoAccessTraceResult.serialize(),
602-
fetchMetrics: result.fetchMetrics,
603606
}
604607
}
605608

packages/next/src/server/app-render/app-render.tsx

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,8 @@ import { parseLoaderTree } from './parse-loader-tree'
181181
import {
182182
createPrerenderResumeDataCache,
183183
createRenderResumeDataCache,
184+
type PrerenderResumeDataCache,
185+
type RenderResumeDataCache,
184186
} from '../resume-data-cache/resume-data-cache'
185187
import type { MetadataErrorType } from '../../lib/metadata/resolve-metadata'
186188
import isError from '../../lib/is-error'
@@ -714,6 +716,7 @@ async function warmupDevRender(
714716
stale: INFINITE_CACHE,
715717
tags: [],
716718
prerenderResumeDataCache,
719+
renderResumeDataCache: null,
717720
hmrRefreshHash: req.cookies[NEXT_HMR_REFRESH_HASH_COOKIE],
718721
}
719722

@@ -750,7 +753,7 @@ async function warmupDevRender(
750753
// lift the warmup pathway outside of renderToHTML... but for now this suffices
751754
return new FlightRenderResult('', {
752755
fetchMetrics: workStore.fetchMetrics,
753-
devRenderResumeDataCache: createRenderResumeDataCache(
756+
renderResumeDataCache: createRenderResumeDataCache(
754757
prerenderResumeDataCache
755758
),
756759
})
@@ -1493,12 +1496,15 @@ async function renderToHTMLOrFlightImpl(
14931496
}
14941497
}
14951498

1499+
if (response.renderResumeDataCache) {
1500+
metadata.renderResumeDataCache = response.renderResumeDataCache
1501+
}
1502+
14961503
return new RenderResult(await streamToString(response.stream), options)
14971504
} else {
14981505
// We're rendering dynamically
14991506
const renderResumeDataCache =
1500-
renderOpts.devRenderResumeDataCache ??
1501-
postponedState?.renderResumeDataCache
1507+
renderOpts.renderResumeDataCache ?? postponedState?.renderResumeDataCache
15021508

15031509
const rootParams = getRootParams(loaderTree, ctx.getDynamicParamFromSegment)
15041510
const requestStore = createRequestStoreForRender(
@@ -1696,7 +1702,7 @@ export const renderToHTMLOrFlight: AppPageRender = (
16961702

16971703
if (
16981704
postponedState?.renderResumeDataCache &&
1699-
renderOpts.devRenderResumeDataCache
1705+
renderOpts.renderResumeDataCache
17001706
) {
17011707
throw new InvariantError(
17021708
'postponed state and dev warmup immutable resume data cache should not be provided together'
@@ -2310,6 +2316,7 @@ async function spawnDynamicValidationInDev(
23102316
stale: INFINITE_CACHE,
23112317
tags: [...implicitTags.tags],
23122318
prerenderResumeDataCache,
2319+
renderResumeDataCache: null,
23132320
hmrRefreshHash,
23142321
}
23152322

@@ -2416,6 +2423,7 @@ async function spawnDynamicValidationInDev(
24162423
stale: INFINITE_CACHE,
24172424
tags: [...implicitTags.tags],
24182425
prerenderResumeDataCache,
2426+
renderResumeDataCache: null,
24192427
hmrRefreshHash: undefined,
24202428
}
24212429

@@ -2501,6 +2509,7 @@ async function spawnDynamicValidationInDev(
25012509
stale: INFINITE_CACHE,
25022510
tags: [...implicitTags.tags],
25032511
prerenderResumeDataCache,
2512+
renderResumeDataCache: null,
25042513
hmrRefreshHash,
25052514
}
25062515

@@ -2563,6 +2572,7 @@ async function spawnDynamicValidationInDev(
25632572
stale: INFINITE_CACHE,
25642573
tags: [...implicitTags.tags],
25652574
prerenderResumeDataCache,
2575+
renderResumeDataCache: null,
25662576
hmrRefreshHash,
25672577
}
25682578

@@ -2673,6 +2683,7 @@ type PrerenderToStreamResult = {
26732683
collectedExpire: number
26742684
collectedStale: number
26752685
collectedTags: null | string[]
2686+
renderResumeDataCache?: RenderResumeDataCache
26762687
}
26772688

26782689
/**
@@ -2866,11 +2877,22 @@ async function prerenderToStream(
28662877
// to cut the render off.
28672878
const cacheSignal = new CacheSignal()
28682879

2869-
// The resume data cache here should use a fresh instance as it's
2870-
// performing a fresh prerender. If we get to implementing the
2871-
// prerendering of an already prerendered page, we should use the passed
2872-
// resume data cache instead.
2873-
const prerenderResumeDataCache = createPrerenderResumeDataCache()
2880+
let resumeDataCache: RenderResumeDataCache | PrerenderResumeDataCache
2881+
let renderResumeDataCache: RenderResumeDataCache | null = null
2882+
let prerenderResumeDataCache: PrerenderResumeDataCache | null = null
2883+
2884+
if (renderOpts.renderResumeDataCache) {
2885+
// If a prefilled immutable render resume data cache is provided, e.g.
2886+
// when prerendering an optional fallback shell after having prerendered
2887+
// pages with defined params, we use this instead of a prerender resume
2888+
// data cache.
2889+
resumeDataCache = renderResumeDataCache =
2890+
renderOpts.renderResumeDataCache
2891+
} else {
2892+
// Otherwise we create a new mutable prerender resume data cache.
2893+
resumeDataCache = prerenderResumeDataCache =
2894+
createPrerenderResumeDataCache()
2895+
}
28742896

28752897
const initialServerPrerenderStore: PrerenderStore = (prerenderStore = {
28762898
type: 'prerender',
@@ -2889,6 +2911,7 @@ async function prerenderToStream(
28892911
stale: INFINITE_CACHE,
28902912
tags: [...implicitTags.tags],
28912913
prerenderResumeDataCache,
2914+
renderResumeDataCache,
28922915
hmrRefreshHash: undefined,
28932916
})
28942917

@@ -3011,6 +3034,7 @@ async function prerenderToStream(
30113034
stale: INFINITE_CACHE,
30123035
tags: [...implicitTags.tags],
30133036
prerenderResumeDataCache,
3037+
renderResumeDataCache,
30143038
hmrRefreshHash: undefined,
30153039
}
30163040

@@ -3096,6 +3120,7 @@ async function prerenderToStream(
30963120
stale: INFINITE_CACHE,
30973121
tags: [...implicitTags.tags],
30983122
prerenderResumeDataCache,
3123+
renderResumeDataCache,
30993124
hmrRefreshHash: undefined,
31003125
})
31013126

@@ -3166,6 +3191,7 @@ async function prerenderToStream(
31663191
stale: INFINITE_CACHE,
31673192
tags: [...implicitTags.tags],
31683193
prerenderResumeDataCache,
3194+
renderResumeDataCache,
31693195
hmrRefreshHash: undefined,
31703196
}
31713197

@@ -3267,13 +3293,12 @@ async function prerenderToStream(
32673293
metadata.postponed = await getDynamicHTMLPostponedState(
32683294
postponed,
32693295
fallbackRouteParams,
3270-
prerenderResumeDataCache
3296+
resumeDataCache
32713297
)
32723298
} else {
32733299
// Dynamic Data case
3274-
metadata.postponed = await getDynamicDataPostponedState(
3275-
prerenderResumeDataCache
3276-
)
3300+
metadata.postponed =
3301+
await getDynamicDataPostponedState(resumeDataCache)
32773302
}
32783303
reactServerResult.consume()
32793304
return {
@@ -3292,6 +3317,7 @@ async function prerenderToStream(
32923317
collectedExpire: finalServerPrerenderStore.expire,
32933318
collectedStale: selectStaleTime(finalServerPrerenderStore.stale),
32943319
collectedTags: finalServerPrerenderStore.tags,
3320+
renderResumeDataCache: createRenderResumeDataCache(resumeDataCache),
32953321
}
32963322
} else {
32973323
// Static case
@@ -3355,6 +3381,7 @@ async function prerenderToStream(
33553381
collectedExpire: finalServerPrerenderStore.expire,
33563382
collectedStale: selectStaleTime(finalServerPrerenderStore.stale),
33573383
collectedTags: finalServerPrerenderStore.tags,
3384+
renderResumeDataCache: createRenderResumeDataCache(resumeDataCache),
33583385
}
33593386
}
33603387
} else if (experimental.isRoutePPREnabled) {

0 commit comments

Comments
 (0)