Skip to content

Commit

Permalink
[ppr] Resume Data Cache (#72161)
Browse files Browse the repository at this point in the history
This adds a new ResumeDataCache concept to Next.js. This cache is persisted during production, and used for pre-seeding during development. Every route will generate its own resume data cache that will be used when partial prerendering is enabled. This lets us fill the caches during the prerender, and re-use those cache entries every time the page is resumed. This allows a future where we can reuse stale static shells while also reusing the resume data cache.
  • Loading branch information
wyattjoh authored Nov 5, 2024
1 parent 1d45e0b commit 2f43ba5
Show file tree
Hide file tree
Showing 27 changed files with 690 additions and 249 deletions.
20 changes: 18 additions & 2 deletions packages/next/src/export/routes/app-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
RSC_SUFFIX,
RSC_SEGMENTS_DIR_SUFFIX,
RSC_SEGMENT_SUFFIX,
NEXT_STATIC_DATA_CACHE_SUFFIX,
} from '../../lib/constants'
import { hasNextSupport } from '../../server/ci-info'
import { lazyRenderAppPage } from '../../server/route-modules/app-page/module.render'
Expand All @@ -27,6 +28,7 @@ import type { WorkStore } from '../../server/app-render/work-async-storage.exter
import type { FallbackRouteParams } from '../../server/request/fallback-params'
import { AfterRunner } from '../../server/after/run-with-after'
import type { RequestLifecycleOpts } from '../../server/base-server'
import { stringifyResumeDataCache } from '../../server/resume-data-cache/serialization'

export const enum ExportedAppPageFiles {
HTML = 'HTML',
Expand All @@ -35,6 +37,7 @@ export const enum ExportedAppPageFiles {
PREFETCH_FLIGHT_SEGMENT = 'PREFETCH_FLIGHT_SEGMENT',
META = 'META',
POSTPONED = 'POSTPONED',
RESUME_CACHE = 'RESUME_CACHE',
}

export async function prospectiveRenderAppPage(
Expand Down Expand Up @@ -66,7 +69,9 @@ export async function prospectiveRenderAppPage(
waitUntil: afterRunner.context.waitUntil,
onClose: afterRunner.context.onClose,
onAfterTaskError: afterRunner.context.onTaskError,
}
},
undefined,
false
)

// TODO(after): if we abort a prerender because of an error in an after-callback
Expand Down Expand Up @@ -126,7 +131,9 @@ export async function exportAppPage(
pathname,
query,
fallbackRouteParams,
renderOpts
renderOpts,
undefined,
false
)

const html = result.toUnchunkedString()
Expand All @@ -143,6 +150,7 @@ export async function exportAppPage(
fetchTags,
fetchMetrics,
segmentFlightData,
immutableResumeDataCache,
} = metadata

// Ensure we don't postpone without having PPR enabled.
Expand Down Expand Up @@ -249,6 +257,14 @@ export async function exportAppPage(
'utf8'
)

if (immutableResumeDataCache) {
await fileWriter(
ExportedAppPageFiles.RESUME_CACHE,
htmlFilepath.replace(/\.html$/, NEXT_STATIC_DATA_CACHE_SUFFIX),
await stringifyResumeDataCache(immutableResumeDataCache)
)
}

const isParallelRoute = /\/@\w+/.test(page)
const isNonSuccessfulStatusCode = res.statusCode > 300

Expand Down
26 changes: 10 additions & 16 deletions packages/next/src/export/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ import {
type FallbackRouteParams,
} from '../server/request/fallback-params'
import { needsExperimentalReact } from '../lib/needs-experimental-react'
import { runWithCacheScope } from '../server/async-storage/cache-scope.external'
import type { AppRouteRouteModule } from '../server/route-modules/app-route/module.compiled'
import { isStaticGenBailoutError } from '../client/components/static-generation-bailout'

Expand Down Expand Up @@ -468,26 +467,21 @@ export async function exportPages(

return { result, path, pageKey }
}
// for each build worker we share one dynamic IO cache scope
// this is only leveraged if the flag is enabled
const dynamicIOCacheScope = new Map()

await runWithCacheScope({ cache: dynamicIOCacheScope }, async () => {
for (let i = 0; i < paths.length; i += maxConcurrency) {
const subset = paths.slice(i, i + maxConcurrency)
for (let i = 0; i < paths.length; i += maxConcurrency) {
const subset = paths.slice(i, i + maxConcurrency)

const subsetResults = await Promise.all(
subset.map((path) =>
exportPageWithRetry(
path,
nextConfig.experimental.staticGenerationRetryCount ?? 1
)
const subsetResults = await Promise.all(
subset.map((path) =>
exportPageWithRetry(
path,
nextConfig.experimental.staticGenerationRetryCount ?? 1
)
)
)

results.push(...subsetResults)
}
})
results.push(...subsetResults)
}

return results
}
Expand Down
1 change: 1 addition & 0 deletions packages/next/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const ACTION_SUFFIX = '.action'
export const NEXT_DATA_SUFFIX = '.json'
export const NEXT_META_SUFFIX = '.meta'
export const NEXT_BODY_SUFFIX = '.body'
export const NEXT_STATIC_DATA_CACHE_SUFFIX = '.rdc.json'

export const NEXT_CACHE_TAGS_HEADER = 'x-next-cache-tags'
export const NEXT_CACHE_SOFT_TAGS_HEADER = 'x-next-cache-soft-tags'
Expand Down
Loading

0 comments on commit 2f43ba5

Please sign in to comment.