Skip to content

Commit 3d29c27

Browse files
authored
[Prefetching] Fix: Read segment data from correct source (#72427)
In the last PR (#72367) I got confused and incorrectly assumed that initialRSCPayload was a client Flight value, not a server value. Which meant it was actually re-rendering the Server Components again, rather than reusing the result from the page render. Instead, I need to decode this value from the full page Flight stream, like I did originally. (Except we still don't need to do it once per segment, just once per page.) To avoid another unecessary decoding of the page stream, I've moved the segment rendering tasks so that they are spawned from inside the render that generates the tree metadata response. The size of the diff is bit misleading; mostly this involves just rearranging the code so that it executes in a slightly different order.
1 parent 7a6b20c commit 3d29c27

File tree

2 files changed

+123
-94
lines changed

2 files changed

+123
-94
lines changed

packages/next/src/server/app-render/app-render.tsx

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2761,7 +2761,6 @@ async function prerenderToStream(
27612761
const flightData = await streamToBuffer(reactServerResult.asStream())
27622762
metadata.flightData = flightData
27632763
metadata.segmentFlightData = await collectSegmentData(
2764-
finalAttemptRSCPayload,
27652764
flightData,
27662765
finalRenderPrerenderStore,
27672766
ComponentMod,
@@ -3228,7 +3227,6 @@ async function prerenderToStream(
32283227
)
32293228
metadata.flightData = flightData
32303229
metadata.segmentFlightData = await collectSegmentData(
3231-
finalServerPayload,
32323230
flightData,
32333231
finalClientPrerenderStore,
32343232
ComponentMod,
@@ -3361,7 +3359,6 @@ async function prerenderToStream(
33613359
if (shouldGenerateStaticFlightData(workStore)) {
33623360
metadata.flightData = flightData
33633361
metadata.segmentFlightData = await collectSegmentData(
3364-
RSCPayload,
33653362
flightData,
33663363
ssrPrerenderStore,
33673364
ComponentMod,
@@ -3554,7 +3551,6 @@ async function prerenderToStream(
35543551
const flightData = await streamToBuffer(reactServerResult.asStream())
35553552
metadata.flightData = flightData
35563553
metadata.segmentFlightData = await collectSegmentData(
3557-
RSCPayload,
35583554
flightData,
35593555
prerenderLegacyStore,
35603556
ComponentMod,
@@ -3709,7 +3705,6 @@ async function prerenderToStream(
37093705
)
37103706
metadata.flightData = flightData
37113707
metadata.segmentFlightData = await collectSegmentData(
3712-
errorRSCPayload,
37133708
flightData,
37143709
prerenderLegacyStore,
37153710
ComponentMod,
@@ -3843,7 +3838,6 @@ const getGlobalErrorStyles = async (
38433838
}
38443839

38453840
async function collectSegmentData(
3846-
rscPayload: InitialRSCPayload,
38473841
fullPageDataBuffer: Buffer,
38483842
prerenderStore: PrerenderStore,
38493843
ComponentMod: AppPageModule,
@@ -3870,18 +3864,6 @@ async function collectSegmentData(
38703864
return
38713865
}
38723866

3873-
// FlightDataPath is an unsound type, hence the additional checks.
3874-
const flightDataPaths = rscPayload.f
3875-
if (flightDataPaths.length !== 1 && flightDataPaths[0].length !== 3) {
3876-
console.error(
3877-
'Internal Next.js error: InitialRSCPayload does not match the expected ' +
3878-
'shape for a prerendered page during segment prefetch generation.'
3879-
)
3880-
return
3881-
}
3882-
const routeTree: FlightRouterState = flightDataPaths[0][0]
3883-
const seedData: CacheNodeSeedData = flightDataPaths[0][1]
3884-
38853867
// Manifest passed to the Flight client for reading the full-page Flight
38863868
// stream. Based off similar code in use-cache-wrapper.ts.
38873869
const isEdgeRuntime = process.env.NEXT_RUNTIME === 'edge'
@@ -3898,8 +3880,6 @@ async function collectSegmentData(
38983880

38993881
const staleTime = prerenderStore.stale
39003882
return await ComponentMod.collectSegmentData(
3901-
routeTree,
3902-
seedData,
39033883
fullPageDataBuffer,
39043884
staleTime,
39053885
clientReferenceManifest.clientModules as ManifestNode,

packages/next/src/server/app-render/collect-segment-data.tsx

Lines changed: 123 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import type { CacheNodeSeedData, FlightRouterState, Segment } from './types'
1+
import type {
2+
CacheNodeSeedData,
3+
FlightRouterState,
4+
InitialRSCPayload,
5+
Segment,
6+
} from './types'
27
import type { ManifestNode } from '../../build/webpack/plugins/flight-manifest-plugin'
38

49
// eslint-disable-next-line import/no-extraneous-dependencies
@@ -53,56 +58,44 @@ type SegmentPrefetch = {
5358
}
5459

5560
export async function collectSegmentData(
56-
flightRouterState: FlightRouterState,
57-
seedData: CacheNodeSeedData,
5861
fullPageDataBuffer: Buffer,
5962
staleTime: number,
6063
clientModules: ManifestNode,
6164
serverConsumerManifest: any
6265
): Promise<Map<string, Buffer>> {
6366
// Traverse the router tree and generate a prefetch response for each segment.
6467

68+
// A mutable map to collect the results as we traverse the route tree.
69+
const resultMap = new Map<string, Buffer>()
70+
6571
// Before we start, warm up the module cache by decoding the page data once.
6672
// Then we can assume that any remaining async tasks that occur the next time
6773
// are due to hanging promises caused by dynamic data access. Note we only
6874
// have to do this once per page, not per individual segment.
6975
//
70-
// Based on similar strategy in warmFlightResponse.
7176
try {
7277
await createFromReadableStream(streamFromBuffer(fullPageDataBuffer), {
7378
serverConsumerManifest,
7479
})
7580
await waitAtLeastOneReactRenderTask()
7681
} catch {}
7782

78-
// A mutable map to collect the results as we traverse the route tree.
79-
const resultMap = new Map<string, Buffer>()
80-
81-
const tree = await collectSegmentDataImpl(
82-
flightRouterState,
83-
seedData,
84-
fullPageDataBuffer,
85-
clientModules,
86-
serverConsumerManifest,
87-
'',
88-
'',
89-
resultMap
90-
)
91-
92-
// Render the route tree to a special `/_tree` segment.
93-
const treePrefetch: RootTreePrefetch = {
94-
tree,
95-
staleTime,
96-
}
83+
// Generate a stream for the route tree prefetch. While we're walking the
84+
// tree, we'll also spawn additional tasks to generate the segment prefetches.
85+
// The promises for these tasks are pushed to a mutable array that we will
86+
// await once the route tree is fully rendered.
87+
const segmentTasks: Array<Promise<[string, Buffer]>> = []
9788
const treeStream = await renderToReadableStream(
98-
// SegmentPrefetch is not a valid return type for a React component, but
89+
// RootTreePrefetch is not a valid return type for a React component, but
9990
// we need to use a component so that when we decode the original stream
10091
// inside of it, the side effects are transferred to the new stream.
10192
// @ts-expect-error
10293
<PrefetchTreeData
10394
fullPageDataBuffer={fullPageDataBuffer}
10495
serverConsumerManifest={serverConsumerManifest}
105-
treePrefetch={treePrefetch}
96+
clientModules={clientModules}
97+
staleTime={staleTime}
98+
segmentTasks={segmentTasks}
10699
/>,
107100
clientModules,
108101
{
@@ -116,12 +109,80 @@ export async function collectSegmentData(
116109
},
117110
}
118111
)
112+
113+
// Write the route tree to a special `/_tree` segment.
119114
const treeBuffer = await streamToBuffer(treeStream)
120115
resultMap.set('/_tree', treeBuffer)
121116

117+
// Now that we've finished rendering the route tree, all the segment tasks
118+
// should have been spawned. Await them in parallel and write the segment
119+
// prefetches to the result map.
120+
for (const [segmentPath, buffer] of await Promise.all(segmentTasks)) {
121+
resultMap.set(segmentPath, buffer)
122+
}
123+
122124
return resultMap
123125
}
124126

127+
async function PrefetchTreeData({
128+
fullPageDataBuffer,
129+
serverConsumerManifest,
130+
clientModules,
131+
staleTime,
132+
segmentTasks,
133+
}: {
134+
fullPageDataBuffer: Buffer
135+
serverConsumerManifest: any
136+
clientModules: ManifestNode
137+
staleTime: number
138+
segmentTasks: Array<Promise<[string, Buffer]>>
139+
}): Promise<RootTreePrefetch | null> {
140+
// We're currently rendering a Flight response for the route tree prefetch.
141+
// Inside this component, decode the Flight stream for the whole page. This is
142+
// a hack to transfer the side effects from the original Flight stream (e.g.
143+
// Float preloads) onto the Flight stream for the tree prefetch.
144+
// TODO: React needs a better way to do this. Needed for Server Actions, too.
145+
const initialRSCPayload: InitialRSCPayload = await createFromReadableStream(
146+
streamFromBuffer(fullPageDataBuffer),
147+
{
148+
serverConsumerManifest,
149+
}
150+
)
151+
152+
// FlightDataPath is an unsound type, hence the additional checks.
153+
const flightDataPaths = initialRSCPayload.f
154+
if (flightDataPaths.length !== 1 && flightDataPaths[0].length !== 3) {
155+
console.error(
156+
'Internal Next.js error: InitialRSCPayload does not match the expected ' +
157+
'shape for a prerendered page during segment prefetch generation.'
158+
)
159+
return null
160+
}
161+
const flightRouterState: FlightRouterState = flightDataPaths[0][0]
162+
const seedData: CacheNodeSeedData = flightDataPaths[0][1]
163+
164+
// Compute the route metadata tree by traversing the FlightRouterState. As we
165+
// walk the tree, we will also spawn a task to produce a prefetch response for
166+
// each segment.
167+
const tree = await collectSegmentDataImpl(
168+
flightRouterState,
169+
seedData,
170+
fullPageDataBuffer,
171+
clientModules,
172+
serverConsumerManifest,
173+
'',
174+
'',
175+
segmentTasks
176+
)
177+
178+
// Render the route tree to a special `/_tree` segment.
179+
const treePrefetch: RootTreePrefetch = {
180+
tree,
181+
staleTime,
182+
}
183+
return treePrefetch
184+
}
185+
125186
async function collectSegmentDataImpl(
126187
route: FlightRouterState,
127188
seedData: CacheNodeSeedData,
@@ -130,7 +191,7 @@ async function collectSegmentDataImpl(
130191
serverConsumerManifest: any,
131192
segmentPathStr: string,
132193
accessToken: string,
133-
segmentBufferMap: Map<string, Buffer>
194+
segmentTasks: Array<Promise<[string, Buffer]>>
134195
): Promise<TreePrefetch> {
135196
// Metadata about the segment. Sent as part of the tree prefetch. Null if
136197
// there are no children.
@@ -168,7 +229,7 @@ async function collectSegmentDataImpl(
168229
serverConsumerManifest,
169230
childSegmentPathStr,
170231
childAccessToken,
171-
segmentBufferMap
232+
segmentTasks
172233
)
173234
if (slotMetadata === null) {
174235
slotMetadata = {}
@@ -181,6 +242,39 @@ async function collectSegmentDataImpl(
181242
childAccessTokens[parallelRouteKey] = childAccessToken
182243
}
183244

245+
// Spawn a task to write the segment data to a new Flight stream.
246+
segmentTasks.push(
247+
// Since we're already in the middle of a render, wait until after the
248+
// current task to escape the current rendering context.
249+
waitAtLeastOneReactRenderTask().then(() =>
250+
renderSegmentPrefetch(
251+
seedData,
252+
segmentPathStr,
253+
accessToken,
254+
childAccessTokens,
255+
clientModules
256+
)
257+
)
258+
)
259+
260+
// Metadata about the segment. Sent to the client as part of the
261+
// tree prefetch.
262+
const segment = route[0]
263+
const isRootLayout = route[4]
264+
return {
265+
key: segmentPathStr === '' ? '/' : segmentPathStr,
266+
slots: slotMetadata,
267+
extra: [segment, isRootLayout === true],
268+
}
269+
}
270+
271+
async function renderSegmentPrefetch(
272+
seedData: CacheNodeSeedData,
273+
segmentPathStr: string,
274+
accessToken: string,
275+
childAccessTokens: { [parallelRouteKey: string]: string } | null,
276+
clientModules: ManifestNode
277+
): Promise<[string, Buffer]> {
184278
// Render the segment data to a stream.
185279
// In the future, this is where we can include additional metadata, like the
186280
// stale time and cache tags.
@@ -210,7 +304,7 @@ async function collectSegmentDataImpl(
210304
const segmentBuffer = await streamToBuffer(segmentStream)
211305
// Add the buffer to the result map.
212306
if (segmentPathStr === '') {
213-
segmentBufferMap.set('/', segmentBuffer)
307+
return ['/', segmentBuffer]
214308
} else {
215309
// The access token is appended to the end of the segment name. To request
216310
// a segment, the client sends a header like:
@@ -220,53 +314,8 @@ async function collectSegmentDataImpl(
220314
// The segment path is provided by the tree prefetch, and the access
221315
// token is provided in the parent layout's data.
222316
const fullPath = `${segmentPathStr}.${accessToken}`
223-
segmentBufferMap.set(fullPath, segmentBuffer)
317+
return [fullPath, segmentBuffer]
224318
}
225-
226-
// Metadata about the segment. Sent to the client as part of the
227-
// tree prefetch.
228-
const segment = route[0]
229-
const isRootLayout = route[4]
230-
return {
231-
key: segmentPathStr === '' ? '/' : segmentPathStr,
232-
slots: slotMetadata,
233-
extra: [segment, isRootLayout === true],
234-
}
235-
}
236-
237-
async function PrefetchTreeData({
238-
fullPageDataBuffer,
239-
serverConsumerManifest,
240-
treePrefetch,
241-
}: {
242-
fullPageDataBuffer: Buffer
243-
serverConsumerManifest: any
244-
treePrefetch: RootTreePrefetch
245-
}): Promise<RootTreePrefetch | null> {
246-
// We're currently rendering a Flight response for a segment prefetch. Inside
247-
// this component, decode the Flight stream for the whole page. This is a hack
248-
// to transfer the side effects from the original Flight stream (e.g. Float
249-
// preloads) onto the Flight stream for the tree prefetch.
250-
// TODO: React needs a better way to do this. Needed for Server Actions, too.
251-
252-
const replayConsoleLogs = true
253-
await Promise.race([
254-
createFromReadableStream(streamFromBuffer(fullPageDataBuffer), {
255-
serverConsumerManifest,
256-
replayConsoleLogs,
257-
}),
258-
259-
// If the page contains dynamic data, the stream will hang indefinitely. So,
260-
// at the end of the current task, stop waiting and proceed rendering. This
261-
// is similar to the AbortSignal strategy we use for generating segment
262-
// data, except we don't actually want or need to abort the outer stream in
263-
// this case.
264-
waitAtLeastOneReactRenderTask(),
265-
])
266-
267-
// By this point the side effects have been transfered and we can render the
268-
// tree metadata.
269-
return treePrefetch
270319
}
271320

272321
// TODO: Consider updating or unifying this encoding logic for segments with

0 commit comments

Comments
 (0)