From 1c6fb80b322b9fd381157b6f6a95613d0fc1b29b Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Thu, 10 Oct 2024 12:04:57 -0400 Subject: [PATCH] Export segment prefetch data during build Implements exporting segment prefetch data during build. Although the goal is to support individual segments being requested by the client, all of the segments for a page are generated simultaneously, including during revalidations. This is to ensure consistency, because it's possible for a mismatch between a layout and page segment can cause the client to error during rendering. We want to preserve the ability of the client to recover from such a mismatch by re-requesting all the segments to get a consistent view of the page. The segments data is generated during the same phase that we currently generate prefetch data for the entire page. In this commit, I have not yet implemented the actual segment generation; I'm using placeholder data for now. I've only implemented the plumbing of the data through the build process. I will implement the segment generation in a separate PR. --- packages/next/src/export/routes/app-page.ts | 35 +++++++++++++++++++ packages/next/src/export/routes/types.ts | 1 + packages/next/src/lib/constants.ts | 2 ++ .../next/src/server/app-render/app-render.tsx | 26 ++++++++++++++ packages/next/src/server/render-result.ts | 2 ++ 5 files changed, 66 insertions(+) diff --git a/packages/next/src/export/routes/app-page.ts b/packages/next/src/export/routes/app-page.ts index d8ce8ed29a815..1b27efc4b5f18 100644 --- a/packages/next/src/export/routes/app-page.ts +++ b/packages/next/src/export/routes/app-page.ts @@ -14,6 +14,8 @@ import { NEXT_META_SUFFIX, RSC_PREFETCH_SUFFIX, RSC_SUFFIX, + RSC_SEGMENTS_DIR_SUFFIX, + RSC_SEGMENT_SUFFIX, } from '../../lib/constants' import { hasNextSupport } from '../../server/ci-info' import { lazyRenderAppPage } from '../../server/route-modules/app-page/module.render' @@ -28,6 +30,7 @@ export const enum ExportedAppPageFiles { HTML = 'HTML', FLIGHT = 'FLIGHT', PREFETCH_FLIGHT = 'PREFETCH_FLIGHT', + PREFETCH_FLIGHT_SEGMENT = 'PREFETCH_FLIGHT_SEGMENT', META = 'META', POSTPONED = 'POSTPONED', } @@ -76,6 +79,7 @@ export async function exportAppPage( postponed, fetchTags, fetchMetrics, + segmentFlightData, } = metadata // Ensure we don't postpone without having PPR enabled. @@ -112,6 +116,7 @@ export async function exportAppPage( throw new Error(`Invariant: failed to get page data for ${path}`) } + let segmentPaths if (flightData) { // If PPR is enabled, we want to emit a prefetch rsc file for the page // instead of the standard rsc. This is because the standard rsc will @@ -120,11 +125,40 @@ export async function exportAppPage( if (renderOpts.experimental.isRoutePPREnabled) { // If PPR is enabled, we should emit the flight data as the prefetch // payload. + // TODO: This will eventually be replaced by the per-segment prefetch + // output below. await fileWriter( ExportedAppPageFiles.PREFETCH_FLIGHT, htmlFilepath.replace(/\.html$/, RSC_PREFETCH_SUFFIX), flightData ) + + if (segmentFlightData) { + // Emit the per-segment prefetch data. We emit them as separate files + // so that the cache handler has the option to treat each as a + // separate entry. + segmentPaths = [] + const segmentsDir = htmlFilepath.replace( + /\.html$/, + RSC_SEGMENTS_DIR_SUFFIX + ) + const tasks = [] + for (const [segmentPath, buffer] of segmentFlightData.entries()) { + segmentPaths.push(segmentPath) + const segmentDataFilePath = + segmentPath === '/' + ? segmentsDir + '/_index' + RSC_SEGMENT_SUFFIX + : segmentsDir + segmentPath + RSC_SEGMENT_SUFFIX + tasks.push( + fileWriter( + ExportedAppPageFiles.PREFETCH_FLIGHT_SEGMENT, + segmentDataFilePath, + buffer + ) + ) + } + await Promise.all(tasks) + } } else { // Writing the RSC payload to a file if we don't have PPR enabled. await fileWriter( @@ -175,6 +209,7 @@ export async function exportAppPage( status, headers, postponed, + segmentPaths, } await fileWriter( diff --git a/packages/next/src/export/routes/types.ts b/packages/next/src/export/routes/types.ts index 0ae9333c481a5..f8ea7eeec9352 100644 --- a/packages/next/src/export/routes/types.ts +++ b/packages/next/src/export/routes/types.ts @@ -4,4 +4,5 @@ export type RouteMetadata = { status: number | undefined headers: OutgoingHttpHeaders | undefined postponed: string | undefined + segmentPaths: Array | undefined } diff --git a/packages/next/src/lib/constants.ts b/packages/next/src/lib/constants.ts index 613ca1eca3901..9dd0a25993877 100644 --- a/packages/next/src/lib/constants.ts +++ b/packages/next/src/lib/constants.ts @@ -8,6 +8,8 @@ export const PRERENDER_REVALIDATE_ONLY_GENERATED_HEADER = 'x-prerender-revalidate-if-generated' export const RSC_PREFETCH_SUFFIX = '.prefetch.rsc' +export const RSC_SEGMENTS_DIR_SUFFIX = '.segments' +export const RSC_SEGMENT_SUFFIX = '.segment.rsc' export const RSC_SUFFIX = '.rsc' export const ACTION_SUFFIX = '.action' export const NEXT_DATA_SUFFIX = '.json' diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx index 62523bb83a454..b09efbb12f296 100644 --- a/packages/next/src/server/app-render/app-render.tsx +++ b/packages/next/src/server/app-render/app-render.tsx @@ -1130,6 +1130,32 @@ async function renderToHTMLOrFlightImpl( } } + // Per-segment prefetch data + // + // All of the segments for a page are generated simultaneously, including + // during revalidations. This is to ensure consistency, because it's + // possible for a mismatch between a layout and page segment can cause the + // client to error during rendering. We want to preserve the ability of the + // client to recover from such a mismatch by re-requesting all the segments + // to get a consistent view of the page. + // + // TODO (Per Segment Prefetching): This is placeholder data. Populate with + // the actual data generated during prerender. + if (renderOpts.experimental.isRoutePPREnabled === true) { + const placeholder = Buffer.from( + 'TODO (Per Segment Prefetching): Not yet implemented\n' + ) + metadata.segmentFlightData = new Map([ + // Root segment + ['/', placeholder], + ['/blog', placeholder], + // TODO: Update the client to use the same encoding for segment paths that + // we use here, so we don't have to convert between them. Needs to be + // filesystem safe. + ['/blog/[post]-1-d', placeholder], + ]) + } + return new RenderResult(await streamToString(response.stream), options) } else { // We're rendering dynamically diff --git a/packages/next/src/server/render-result.ts b/packages/next/src/server/render-result.ts index 448a0431795e6..3dd4b2a5eef16 100644 --- a/packages/next/src/server/render-result.ts +++ b/packages/next/src/server/render-result.ts @@ -32,6 +32,8 @@ export type AppPageRenderResultMetadata = { headers?: OutgoingHttpHeaders fetchTags?: string fetchMetrics?: FetchMetrics + + segmentFlightData?: Map } export type PagesRenderResultMetadata = {