[ppr] Fix PPR detection for groups (#11625)

Previously, PPR was either all on, or all off. As we've added support for incremental adoption of PPR, the logic for determining if there's a grouping issue had to be adjusted.

This now only considers a group as supporting PPR if a route in the group supports PPR. As we also use this as a grouping key, this still ensures we won't have conflicts.
This commit is contained in:
Wyatt Johnson
2024-05-22 22:06:12 -06:00
committed by GitHub
parent 464cb26255
commit 73e558913a
4 changed files with 68 additions and 69 deletions

View File

@@ -0,0 +1,6 @@
---
'@vercel/next': patch
'vercel': patch
---
Support incremental PPR for large applications

View File

@@ -1358,6 +1358,11 @@ export const build: BuildV2 = async ({
experimentalPPRRoutes.add(route);
}
const isAppPPREnabled = requiredServerFilesManifest
? requiredServerFilesManifest.config.experimental?.ppr === true ||
requiredServerFilesManifest.config.experimental?.ppr === 'incremental'
: false;
if (requiredServerFilesManifest) {
if (!routesManifest) {
throw new Error(
@@ -1413,6 +1418,7 @@ export const build: BuildV2 = async ({
hasIsr500Page,
variantsManifest,
experimentalPPRRoutes,
isAppPPREnabled,
});
}
@@ -1932,7 +1938,7 @@ export const build: BuildV2 = async ({
canUsePreviewMode,
bypassToken: prerenderManifest.bypassToken || '',
isServerMode,
experimentalPPRRoutes,
isAppPPREnabled: false,
hasActionOutputSupport: false,
}).then(arr =>
localizeDynamicRoutes(
@@ -1963,7 +1969,7 @@ export const build: BuildV2 = async ({
canUsePreviewMode,
bypassToken: prerenderManifest.bypassToken || '',
isServerMode,
experimentalPPRRoutes,
isAppPPREnabled: false,
hasActionOutputSupport: false,
}).then(arr =>
arr.map(route => {

View File

@@ -144,6 +144,7 @@ export async function serverBuild({
requiredServerFilesManifest,
variantsManifest,
experimentalPPRRoutes,
isAppPPREnabled,
}: {
appPathRoutesManifest?: Record<string, string>;
dynamicPages: string[];
@@ -185,7 +186,15 @@ export async function serverBuild({
requiredServerFilesManifest: NextRequiredServerFilesManifest;
variantsManifest: VariantsManifest | null;
experimentalPPRRoutes: ReadonlySet<string>;
isAppPPREnabled: boolean;
}): Promise<BuildResult> {
if (isAppPPREnabled) {
debug(
'experimentalPPRRoutes',
JSON.stringify(Array.from(experimentalPPRRoutes))
);
}
lambdaPages = Object.assign({}, lambdaPages, lambdaAppPaths);
const experimentalAllowBundling = Boolean(
@@ -217,12 +226,6 @@ export async function serverBuild({
}
}
const experimental = {
ppr:
requiredServerFilesManifest.config.experimental?.ppr === true ||
requiredServerFilesManifest.config.experimental?.ppr === 'incremental',
};
let appRscPrefetches: UnwrapPromise<ReturnType<typeof glob>> = {};
let appBuildTraces: UnwrapPromise<ReturnType<typeof glob>> = {};
let appDir: string | null = null;
@@ -230,7 +233,7 @@ export async function serverBuild({
if (appPathRoutesManifest) {
appDir = path.join(pagesDir, '../app');
appBuildTraces = await glob('**/*.js.nft.json', appDir);
appRscPrefetches = experimental.ppr
appRscPrefetches = isAppPPREnabled
? {}
: await glob(`**/*${RSC_PREFETCH_SUFFIX}`, appDir);
@@ -251,7 +254,7 @@ export async function serverBuild({
if (rewrite.src && rewrite.dest) {
rewrite.src = rewrite.src.replace(
/\/?\(\?:\/\)\?/,
`(?<rscsuff>${experimental.ppr ? '(\\.prefetch)?' : ''}\\.rsc)?(?:/)?`
`(?<rscsuff>${isAppPPREnabled ? '(\\.prefetch)?' : ''}\\.rsc)?(?:/)?`
);
let destQueryIndex = rewrite.dest.indexOf('?');
@@ -934,9 +937,6 @@ export async function serverBuild({
const appRouterStreamingActionLambdaGroups: LambdaGroup[] = [];
for (const group of appRouterLambdaGroups) {
if (!group.isPrerenders || group.isExperimentalPPR) {
group.isStreaming = true;
}
group.isAppRouter = true;
// We create a streaming variant of the Prerender lambda group
@@ -951,9 +951,6 @@ export async function serverBuild({
}
for (const group of appRouteHandlersLambdaGroups) {
if (!group.isPrerenders) {
group.isStreaming = true;
}
group.isAppRouter = true;
group.isAppRouteHandler = true;
}
@@ -984,18 +981,24 @@ export async function serverBuild({
apiLambdaGroups: apiLambdaGroups.map(group => ({
pages: group.pages,
isPrerender: group.isPrerenders,
isStreaming: group.isStreaming,
isExperimentalPPR: group.isExperimentalPPR,
pseudoLayerBytes: group.pseudoLayerBytes,
uncompressedLayerBytes: group.pseudoLayerUncompressedBytes,
})),
pageLambdaGroups: pageLambdaGroups.map(group => ({
pages: group.pages,
isPrerender: group.isPrerenders,
isStreaming: group.isStreaming,
isExperimentalPPR: group.isExperimentalPPR,
pseudoLayerBytes: group.pseudoLayerBytes,
uncompressedLayerBytes: group.pseudoLayerUncompressedBytes,
})),
appRouterLambdaGroups: appRouterLambdaGroups.map(group => ({
pages: group.pages,
isPrerender: group.isPrerenders,
isStreaming: group.isStreaming,
isExperimentalPPR: group.isExperimentalPPR,
pseudoLayerBytes: group.pseudoLayerBytes,
uncompressedLayerBytes: group.pseudoLayerUncompressedBytes,
})),
@@ -1003,6 +1006,8 @@ export async function serverBuild({
appRouterStreamingActionLambdaGroups.map(group => ({
pages: group.pages,
isPrerender: group.isPrerenders,
isStreaming: group.isStreaming,
isExperimentalPPR: group.isExperimentalPPR,
pseudoLayerBytes: group.pseudoLayerBytes,
uncompressedLayerBytes: group.pseudoLayerUncompressedBytes,
})),
@@ -1010,6 +1015,8 @@ export async function serverBuild({
group => ({
pages: group.pages,
isPrerender: group.isPrerenders,
isStreaming: group.isStreaming,
isExperimentalPPR: group.isExperimentalPPR,
pseudoLayerBytes: group.pseudoLayerBytes,
uncompressedLayerBytes: group.pseudoLayerUncompressedBytes,
})
@@ -1189,15 +1196,10 @@ export async function serverBuild({
const lambda = await createLambdaFromPseudoLayers(options);
// This is a PPR lambda if it's an App Page with the PPR experimental flag
// enabled.
const isPPR =
experimental.ppr && group.isAppRouter && !group.isAppRouteHandler;
// If PPR is enabled and this is an App Page, create the non-streaming
// lambda for the page for revalidation.
let revalidate: NodejsLambda | undefined;
if (isPPR) {
if (group.isExperimentalPPR) {
if (!options.isStreaming) {
throw new Error("Invariant: PPR lambda isn't streaming");
}
@@ -1214,14 +1216,11 @@ export async function serverBuild({
// This is the name of the page, where the root is `index`.
const pageName = pageFilename.replace(/\.js$/, '');
// This is the name of the page prefixed with a `/`, where the root is
// `/index`.
const pagePath = path.posix.join('/', pageName);
// This is the routable pathname for the page, where the root is `/`.
const pagePathname = pagePath === '/index' ? '/' : pagePath;
const pagePathname = normalizePage(pageName);
let isPrerender = prerenderRoutes.has(pagePathname);
const isRoutePPREnabled = experimentalPPRRoutes.has(pagePathname);
if (!isPrerender && routesManifest?.i18n) {
isPrerender = routesManifest.i18n.locales.some(locale => {
@@ -1239,7 +1238,7 @@ export async function serverBuild({
}
// If this is a PPR page, then we should prefix the output name.
if (isPPR) {
if (isRoutePPREnabled) {
if (!revalidate) {
throw new Error("Invariant: PPR lambda isn't set");
}
@@ -1320,6 +1319,13 @@ export async function serverBuild({
console.timeEnd(lambdaCreationLabel);
}
if (isAppPPREnabled) {
debug(
'experimentalStreamingLambdaPaths',
JSON.stringify(Array.from(experimentalStreamingLambdaPaths))
);
}
const prerenderRoute = onPrerenderRoute({
appDir,
pagesDir,
@@ -1407,7 +1413,7 @@ export async function serverBuild({
bypassToken: prerenderManifest.bypassToken || '',
isServerMode: true,
dynamicMiddlewareRouteMap: middleware.dynamicRouteMap,
experimentalPPRRoutes,
isAppPPREnabled,
hasActionOutputSupport,
}).then(arr =>
localizeDynamicRoutes(
@@ -1589,7 +1595,7 @@ export async function serverBuild({
if (lambdas[pathname]) {
lambdas[`${pathname}.rsc`] = lambdas[pathname];
if (experimental.ppr) {
if (isAppPPREnabled) {
lambdas[`${pathname}${RSC_PREFETCH_SUFFIX}`] = lambdas[pathname];
}
}
@@ -1597,7 +1603,7 @@ export async function serverBuild({
if (edgeFunctions[pathname]) {
edgeFunctions[`${pathname}.rsc`] = edgeFunctions[pathname];
if (experimental.ppr) {
if (isAppPPREnabled) {
edgeFunctions[`${pathname}${RSC_PREFETCH_SUFFIX}`] =
edgeFunctions[pathname];
}
@@ -1616,7 +1622,7 @@ export async function serverBuild({
'RSC, Next-Router-State-Tree, Next-Router-Prefetch';
const appNotFoundPath = path.posix.join('.', entryDirectory, '_not-found');
if (experimental.ppr && !rscPrefetchHeader) {
if (isAppPPREnabled && !rscPrefetchHeader) {
throw new Error("Invariant: cannot use PPR without 'rsc.prefetchHeader'");
}
@@ -1624,7 +1630,7 @@ export async function serverBuild({
// all the routes that support it (and are listed) have configured lambdas.
// This only applies to routes that do not have fallbacks enabled (these are
// routes that have `dynamicParams = false` defined.
if (experimental.ppr) {
if (isAppPPREnabled) {
for (const { srcRoute, dataRoute, experimentalPPR } of Object.values(
prerenderManifest.staticRoutes
)) {
@@ -1907,7 +1913,7 @@ export async function serverBuild({
...(appDir
? [
...(rscPrefetchHeader && experimental.ppr
...(rscPrefetchHeader && isAppPPREnabled
? [
{
src: `^${path.posix.join('/', entryDirectory, '/')}`,

View File

@@ -192,8 +192,12 @@ function normalizePage(page: string): string {
if (!page.startsWith('/')) {
page = `/${page}`;
}
// remove '/index' from the end
page = page.replace(/\/index$/, '/');
// Replace the `/index` with `/`
if (page === '/index') {
page = '/';
}
return page;
}
@@ -320,8 +324,8 @@ export async function getDynamicRoutes({
bypassToken,
isServerMode,
dynamicMiddlewareRouteMap,
experimentalPPRRoutes,
hasActionOutputSupport,
isAppPPREnabled,
}: {
entryPath: string;
entryDirectory: string;
@@ -333,8 +337,8 @@ export async function getDynamicRoutes({
bypassToken?: string;
isServerMode?: boolean;
dynamicMiddlewareRouteMap?: ReadonlyMap<string, RouteWithSrc>;
experimentalPPRRoutes: ReadonlySet<string>;
hasActionOutputSupport: boolean;
isAppPPREnabled: boolean;
}): Promise<RouteWithSrc[]> {
if (routesManifest) {
switch (routesManifest.version) {
@@ -408,7 +412,7 @@ export async function getDynamicRoutes({
];
}
if (experimentalPPRRoutes.has(page)) {
if (isAppPPREnabled) {
let dest = route.dest?.replace(/($|\?)/, '.prefetch.rsc$1');
if (page === '/' || page === '/index') {
@@ -1504,9 +1508,9 @@ export type LambdaGroup = {
maxDuration?: number;
isAppRouter?: boolean;
isAppRouteHandler?: boolean;
isStreaming?: boolean;
isPrerenders?: boolean;
isExperimentalPPR?: boolean;
readonly isStreaming: boolean;
readonly isPrerenders: boolean;
readonly isExperimentalPPR: boolean;
isActionLambda?: boolean;
isPages?: boolean;
isApiLambda: boolean;
@@ -1561,6 +1565,7 @@ export async function getPageLambdaGroups({
const routeName = normalizePage(page.replace(/\.js$/, ''));
const isPrerenderRoute = prerenderRoutes.has(routeName);
const isExperimentalPPR = experimentalPPRRoutes?.has(routeName) ?? false;
const isStreaming = !isPrerenderRoute || isExperimentalPPR;
let opts: { memory?: number; maxDuration?: number } = {};
@@ -1632,6 +1637,7 @@ export async function getPageLambdaGroups({
...opts,
isPrerenders: isPrerenderRoute,
isExperimentalPPR,
isStreaming,
isApiLambda: !!isApiPage(page),
pseudoLayerBytes: initialPseudoLayer.pseudoLayerBytes,
pseudoLayerUncompressedBytes: initialPseudoLayerUncompressed,
@@ -2219,22 +2225,6 @@ export const onPrerenderRoute =
initialStatus = 404;
}
/**
* If the route key had an `/index` suffix added, we need to note it so we
* can remove it from the output path later accurately.
*/
let addedIndexSuffix = false;
if (isAppPathRoute) {
// for literal index routes we need to append an additional /index
// due to the proxy's normalizing for /index routes
if (routeKey !== '/index' && routeKey.endsWith('/index')) {
routeKey = `${routeKey}/index`;
routeFileNoExt = routeKey;
addedIndexSuffix = true;
}
}
let outputPathPage = path.posix.join(entryDirectory, routeFileNoExt);
if (!isAppPathRoute) {
@@ -2428,7 +2418,7 @@ export const onPrerenderRoute =
// static route first, then try the srcRoute if it doesn't exist. If we
// can't find it at all, this constitutes an error.
experimentalStreamingLambdaPath = experimentalStreamingLambdaPaths.get(
pathnameToOutputName(entryDirectory, routeKey, addedIndexSuffix)
pathnameToOutputName(entryDirectory, routeKey)
);
if (!experimentalStreamingLambdaPath && srcRoute) {
experimentalStreamingLambdaPath =
@@ -2689,19 +2679,10 @@ export function getNextServerPath(nextVersion: string) {
: 'next/dist/next-server/server';
}
function pathnameToOutputName(
entryDirectory: string,
pathname: string,
addedIndexSuffix = false
) {
function pathnameToOutputName(entryDirectory: string, pathname: string) {
if (pathname === '/') {
pathname = '/index';
}
// If the `/index` was added for a route that ended in `/index` we need to
// strip the second one off before joining it with the entryDirectory.
else if (addedIndexSuffix) {
pathname = pathname.replace(/\/index$/, '');
}
return path.posix.join(entryDirectory, pathname);
}