diff --git a/packages/next/src/server/app-render/create-component-tree.tsx b/packages/next/src/server/app-render/create-component-tree.tsx index 12a01c9c63efd..0fbe4ecd0fc68 100644 --- a/packages/next/src/server/app-render/create-component-tree.tsx +++ b/packages/next/src/server/app-render/create-component-tree.tsx @@ -56,7 +56,7 @@ export async function createComponentTree({ ctx: AppRenderContext }): Promise { const { - renderOpts: { nextConfigOutput }, + renderOpts: { nextConfigOutput, experimental }, staticGenerationStore, componentMod: { staticGenerationBailout, @@ -325,12 +325,43 @@ export async function createComponentTree({ // We also want to bail out if there's no Loading component in the tree. let currentStyles = undefined let childCacheNodeSeedData: CacheNodeSeedData | null = null + if ( - !( - isPrefetch && - (Loading || !hasLoadingComponentInTree(parallelRoute)) - ) + // Before PPR, the way instant navigations work in Next.js is we + // prefetch everything up to the first route segment that defines a + // loading.tsx boundary. (We do the same if there's no loading + // boundary in the entire tree, because we don't want to prefetch too + // much) The rest of the tree is defered until the actual navigation. + // It does not take into account whether the data is dynamic — even if + // the tree is completely static, it will still defer everything + // inside the loading boundary. + // + // This behavior predates PPR and is only relevant if the + // PPR flag is not enabled. + isPrefetch && + (Loading || !hasLoadingComponentInTree(parallelRoute)) && + // The approach with PPR is different — loading.tsx behaves like a + // regular Suspense boundary and has no special behavior. + // + // With PPR, we prefetch as deeply as possible, and only defer when + // dynamic data is accessed. If so, we only defer the nearest parent + // Suspense boundary of the dynamic data access, regardless of whether + // the boundary is defined by loading.tsx or a normal + // component in userspace. + // + // NOTE: In practice this usually means we'll end up prefetching more + // than we were before PPR, which may or may not be considered a + // performance regression by some apps. The plan is to address this + // before General Availability of PPR by introducing granular + // per-segment fetching, so we can reuse as much of the tree as + // possible during both prefetches and dynamic navigations. But during + // the beta period, we should be clear about this trade off in our + // communications. + !experimental.ppr ) { + // Don't prefetch this child. This will trigger a lazy fetch by the + // client router. + } else { // Create the child component const { seedData, styles: childComponentStyles } = await createComponentTree({ diff --git a/packages/next/src/server/app-render/walk-tree-with-flight-router-state.tsx b/packages/next/src/server/app-render/walk-tree-with-flight-router-state.tsx index 4bc0b0596ef12..b5fc42b78a96c 100644 --- a/packages/next/src/server/app-render/walk-tree-with-flight-router-state.tsx +++ b/packages/next/src/server/app-render/walk-tree-with-flight-router-state.tsx @@ -58,7 +58,7 @@ export async function walkTreeWithFlightRouterState({ ctx: AppRenderContext }): Promise { const { - renderOpts: { nextFontManifest }, + renderOpts: { nextFontManifest, experimental }, query, isPrefetch, getDynamicParamFromSegment, @@ -111,6 +111,8 @@ export async function walkTreeWithFlightRouterState({ flightRouterState[3] === 'refetch' const shouldSkipComponentTree = + // loading.tsx has no effect on prefetching when PPR is enabled + !experimental.ppr && isPrefetch && !Boolean(components.loading) && (flightRouterState || diff --git a/test/e2e/app-dir/app/index.test.ts b/test/e2e/app-dir/app/index.test.ts index bef69d621e64e..b6a42c932b12d 100644 --- a/test/e2e/app-dir/app/index.test.ts +++ b/test/e2e/app-dir/app/index.test.ts @@ -3,6 +3,11 @@ import { check, getRedboxHeader, hasRedbox, waitFor } from 'next-test-utils' import cheerio from 'cheerio' import stripAnsi from 'strip-ansi' +// TODO: We should decide on an established pattern for gating test assertions +// on experimental flags. For example, as a first step we could all the common +// gates like this one into a single module. +const isPPREnabledByDefault = process.env.__NEXT_EXPERIMENTAL_PPR === 'true' + createNextDescribe( 'app dir - basic', { @@ -550,7 +555,12 @@ createNextDescribe( }) // TODO-APP: Enable in development - ;(isDev ? it.skip : it)( + ;(isDev || + // When PPR is enabled, the shared layouts re-render because we prefetch + // from the root. This will be addressed before GA. + isPPREnabledByDefault + ? it.skip + : it)( 'should not rerender layout when navigating between routes in the same layout', async () => { const browser = await next.browser('/same-layout/first') @@ -1334,7 +1344,12 @@ createNextDescribe( }) // TODO-APP: disable failing test and investigate later - ;(isDev ? it.skip : it)( + ;(isDev || + // When PPR is enabled, the shared layouts re-render because we prefetch + // from the root. This will be addressed before GA. + isPPREnabledByDefault + ? it.skip + : it)( 'should render the template that is a server component and rerender on navigation', async () => { const browser = await next.browser('/template/servercomponent')