Skip to content

Commit

Permalink
Generate per-segment responses for any static page (#73945)
Browse files Browse the repository at this point in the history
Originally I gated per-segment prefetch generation on the PPR flag,
because I thought the client Segment Cache would require PPR to be
enabled on the server. However, since then the strategy has evolved and
I do think we can roll out the Segment Cache independently of PPR.

Dynamic pages without PPR won't be able to take full advantage of the
Segment Cache, but if the page is fully static then there's no reason we
can't implement all the same behavior.

So during per-segment prerendering, I've changed the feature condition
to check for the `clientSegmentCache` flag instead of the PPR one.

---

Additionally, when I broadened the feature check, a failing test
revealed that I neglected to set up error digest decoding correctly, so
the first commit in this stack fixes that.
  • Loading branch information
acdlite authored Dec 17, 2024
1 parent 5b60c13 commit 3ffabd1
Show file tree
Hide file tree
Showing 6 changed files with 50 additions and 14 deletions.
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,
},
}

0 comments on commit 3ffabd1

Please sign in to comment.