Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate per-segment responses for any static page #73945

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/next/src/export/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,7 @@ async function exportAppImpl(
clientTraceMetadata: nextConfig.experimental.clientTraceMetadata,
expireTime: nextConfig.expireTime,
dynamicIO: nextConfig.experimental.dynamicIO ?? false,
clientSegmentCache: nextConfig.experimental.clientSegmentCache ?? false,
inlineCss: nextConfig.experimental.inlineCss ?? false,
authInterrupts: !!nextConfig.experimental.authInterrupts,
},
Expand Down
24 changes: 20 additions & 4 deletions packages/next/src/server/app-render/app-render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4001,10 +4001,7 @@ async function collectSegmentData(
// decomposed into a separate stream per segment.

const clientReferenceManifest = renderOpts.clientReferenceManifest
if (
!clientReferenceManifest ||
renderOpts.experimental.isRoutePPREnabled !== true
) {
if (!clientReferenceManifest || !renderOpts.experimental.clientSegmentCache) {
return
}

Expand All @@ -4022,8 +4019,27 @@ async function collectSegmentData(
serverModuleMap: null,
}

// When dynamicIO is enabled, missing data is encoded to an infinitely hanging
// promise, the absence of which we use to determine if a segment is fully
// static or partially static. However, when dynamicIO is not enabled, this
// trick doesn't work.
//
// So if PPR is enabled, and dynamicIO is not, we have to be conservative and
// assume all segments are partial.
//
// TODO: When PPR is on, we can at least optimize the case where the entire
// page is static. Either by passing that as an argument to this function, or
// by setting a header on the response like the we do for full page RSC
// prefetches today. The latter approach might be simpler since it requires
// less plumbing, and the client has to check the header regardless to see if
// PPR is enabled.
const shouldAssumePartialData =
renderOpts.experimental.isRoutePPREnabled === true && // PPR is enabled
!renderOpts.experimental.dynamicIO // dynamicIO is disabled

const staleTime = prerenderStore.stale
return await ComponentMod.collectSegmentData(
shouldAssumePartialData,
fullPageDataBuffer,
staleTime,
clientReferenceManifest.clientModules as ManifestNode,
Expand Down
35 changes: 25 additions & 10 deletions packages/next/src/server/app-render/collect-segment-data.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
encodeSegment,
ROOT_SEGMENT_KEY,
} from './segment-value-encoding'
import { getDigestForWellKnownError } from './create-error-handler'

// Contains metadata about the route tree. The client must fetch this before
// it can fetch any actual segment data.
Expand Down Expand Up @@ -66,7 +67,17 @@ export type SegmentPrefetch = {
isPartial: boolean
}

function onSegmentPrerenderError(error: unknown) {
const digest = getDigestForWellKnownError(error)
if (digest) {
return digest
}
// We don't need to log the errors because we would have already done that
// when generating the original Flight stream for the whole page.
}

export async function collectSegmentData(
shouldAssumePartialData: boolean,
fullPageDataBuffer: Buffer,
staleTime: number,
clientModules: ManifestNode,
Expand Down Expand Up @@ -110,6 +121,7 @@ export async function collectSegmentData(
// inside of it, the side effects are transferred to the new stream.
// @ts-expect-error
<PrefetchTreeData
shouldAssumePartialData={shouldAssumePartialData}
fullPageDataBuffer={fullPageDataBuffer}
serverConsumerManifest={serverConsumerManifest}
clientModules={clientModules}
Expand All @@ -120,10 +132,7 @@ export async function collectSegmentData(
clientModules,
{
signal: abortController.signal,
onError() {
// Ignore any errors. These would have already been reported when
// we created the full page data.
},
onError: onSegmentPrerenderError,
}
)

Expand All @@ -142,13 +151,15 @@ export async function collectSegmentData(
}

async function PrefetchTreeData({
shouldAssumePartialData,
fullPageDataBuffer,
serverConsumerManifest,
clientModules,
staleTime,
segmentTasks,
onCompletedProcessingRouteTree,
}: {
shouldAssumePartialData: boolean
fullPageDataBuffer: Buffer
serverConsumerManifest: any
clientModules: ManifestNode
Expand Down Expand Up @@ -187,6 +198,7 @@ async function PrefetchTreeData({
// walk the tree, we will also spawn a task to produce a prefetch response for
// each segment.
const tree = await collectSegmentDataImpl(
shouldAssumePartialData,
flightRouterState,
buildId,
seedData,
Expand All @@ -198,7 +210,8 @@ async function PrefetchTreeData({
segmentTasks
)

const isHeadPartial = await isPartialRSCData(head, clientModules)
const isHeadPartial =
shouldAssumePartialData || (await isPartialRSCData(head, clientModules))

// Notify the abort controller that we're done processing the route tree.
// Anything async that happens after this point must be due to hanging
Expand All @@ -217,6 +230,7 @@ async function PrefetchTreeData({
}

async function collectSegmentDataImpl(
shouldAssumePartialData: boolean,
route: FlightRouterState,
buildId: string,
seedData: CacheNodeSeedData | null,
Expand Down Expand Up @@ -249,6 +263,7 @@ async function collectSegmentDataImpl(
parallelRouteKey
)
const childTree = await collectSegmentDataImpl(
shouldAssumePartialData,
childRoute,
buildId,
childSeedData,
Expand All @@ -272,6 +287,7 @@ async function collectSegmentDataImpl(
// current task to escape the current rendering context.
waitAtLeastOneReactRenderTask().then(() =>
renderSegmentPrefetch(
shouldAssumePartialData,
buildId,
seedData,
key,
Expand Down Expand Up @@ -299,6 +315,7 @@ async function collectSegmentDataImpl(
}

async function renderSegmentPrefetch(
shouldAssumePartialData: boolean,
buildId: string,
seedData: CacheNodeSeedData,
key: string,
Expand All @@ -314,7 +331,8 @@ async function renderSegmentPrefetch(
buildId,
rsc,
loading,
isPartial: await isPartialRSCData(rsc, clientModules),
isPartial:
shouldAssumePartialData || (await isPartialRSCData(rsc, clientModules)),
}
// Since all we're doing is decoding and re-encoding a cached prerender, if
// it takes longer than a microtask, it must because of hanging promises
Expand All @@ -326,10 +344,7 @@ async function renderSegmentPrefetch(
clientModules,
{
signal: abortController.signal,
onError() {
// Ignore any errors. These would have already been reported when
// we created the full page data.
},
onError: onSegmentPrerenderError,
}
)
const segmentBuffer = await streamToBuffer(segmentStream)
Expand Down
1 change: 1 addition & 0 deletions packages/next/src/server/app-render/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ export interface RenderOptsPartial {
expireTime: ExpireTime | undefined
clientTraceMetadata: string[] | undefined
dynamicIO: boolean
clientSegmentCache: boolean
inlineCss: boolean
authInterrupts: boolean
}
Expand Down
2 changes: 2 additions & 0 deletions packages/next/src/server/base-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,8 @@ export default abstract class Server<
expireTime: this.nextConfig.expireTime,
clientTraceMetadata: this.nextConfig.experimental.clientTraceMetadata,
dynamicIO: this.nextConfig.experimental.dynamicIO ?? false,
clientSegmentCache:
this.nextConfig.experimental.clientSegmentCache ?? false,
inlineCss: this.nextConfig.experimental.inlineCss ?? false,
authInterrupts: !!this.nextConfig.experimental.authInterrupts,
},
Expand Down
1 change: 1 addition & 0 deletions test/e2e/app-dir/ppr-navigations/simple/next.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
module.exports = {
experimental: {
ppr: true,
clientSegmentCache: true,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious, but why are we only enabling it for this PPR test?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's just temporary so the per-segment-prefetching.test.ts tests still work. I'm just going to delete that test file in an upcoming PR, since we have plenty of coverage via the e2e tests now, and then I can turn this flag off again. I figured it's fine to turn it on here temporarily because the other tests in this directory are fairly basic smoke tests.

},
}
Loading