Skip to content

Commit eecc5f1

Browse files
authored
[Segment Cache] Skip dynamic request if possible (#73540)
During a navigation, if all the data has been prefetched, and the target route does not contain any dynamic data, then we should skip a request to the server. This uses the `isPartial` field I added in the previous PRs to track whether the prefetched data is complete or not.
1 parent 3970d33 commit eecc5f1

File tree

6 files changed

+291
-82
lines changed

6 files changed

+291
-82
lines changed

packages/next/src/client/components/router-reducer/ppr-navigations.ts

Lines changed: 170 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,17 @@ import type { FetchServerResponseResult } from './fetch-server-response'
2020
// request. We can't use the Cache Node tree or Route State tree directly
2121
// because those include reused nodes, too. This tree is discarded as soon as
2222
// the navigation response is received.
23-
type Task = {
23+
export type Task = {
2424
// The router state that corresponds to the tree that this Task represents.
2525
route: FlightRouterState
26-
// This is usually non-null. It represents a brand new Cache Node tree whose
27-
// data is still pending. If it's null, it means there's no pending data but
28-
// the client patched the router state.
26+
// The CacheNode that corresponds to the tree that this Task represents. If
27+
// `children` is null (i.e. if this is a terminal task node), then `node`
28+
// represents a brand new Cache Node tree, which way or may not need to be
29+
// filled with dynamic data from the server.
2930
node: CacheNode | null
31+
// Whether anything in this tree contains dynamic holes that need to be filled
32+
// by the server.
33+
needsDynamicRequest: boolean
3034
children: Map<string, Task> | null
3135
}
3236

@@ -64,7 +68,8 @@ export function updateCacheNodeOnNavigation(
6468
oldRouterState: FlightRouterState,
6569
newRouterState: FlightRouterState,
6670
prefetchData: CacheNodeSeedData | null,
67-
prefetchHead: React.ReactNode | null
71+
prefetchHead: React.ReactNode | null,
72+
isPrefetchHeadPartial: boolean
6873
): Task | null {
6974
// Diff the old and new trees to reuse the shared layouts.
7075
const oldRouterStateChildren = oldRouterState[1]
@@ -96,14 +101,15 @@ export function updateCacheNodeOnNavigation(
96101
} = {}
97102
let taskChildren = null
98103

99-
// For most navigations, we need to issue a "dynamic" request to fetch the
100-
// full RSC data from the server since during rendering, we'll only serve
101-
// the prefetch shell. For some navigations, we re-use the existing cache node
102-
// (via `spawnReusedTask`), and don't actually need fresh data from the server.
103-
// In those cases, we use this `needsDynamicRequest` flag to return a `null`
104-
// cache node, which signals to the caller that we don't need to issue a
105-
// dynamic request. We start off with a `false` value, and then for each parallel
106-
// route, we set it to `true` if we encounter a segment that needs a dynamic request.
104+
// Most navigations require a request to fetch additional data from the
105+
// server, either because the data was not already prefetched, or because the
106+
// target route contains dynamic data that cannot be prefetched.
107+
//
108+
// However, if the target route is fully static, and it's already completely
109+
// loaded into the segment cache, then we can skip the server request.
110+
//
111+
// This starts off as `false`, and is set to `true` if any of the child
112+
// routes requires a dynamic request.
107113
let needsDynamicRequest = false
108114

109115
for (let parallelRouteKey in newRouterStateChildren) {
@@ -144,10 +150,11 @@ export function updateCacheNodeOnNavigation(
144150
taskChild = spawnReusedTask(oldRouterStateChild)
145151
} else {
146152
// There's no currently active segment. Switch to the "create" path.
147-
taskChild = spawnPendingTask(
153+
taskChild = createCacheNodeOnNavigation(
148154
newRouterStateChild,
149155
prefetchDataChild !== undefined ? prefetchDataChild : null,
150-
prefetchHead
156+
prefetchHead,
157+
isPrefetchHeadPartial
151158
)
152159
}
153160
} else if (
@@ -165,24 +172,27 @@ export function updateCacheNodeOnNavigation(
165172
oldRouterStateChild,
166173
newRouterStateChild,
167174
prefetchDataChild,
168-
prefetchHead
175+
prefetchHead,
176+
isPrefetchHeadPartial
169177
)
170178
} else {
171179
// Either there's no existing Cache Node for this segment, or this
172180
// segment doesn't exist in the old Router State tree. Switch to the
173181
// "create" path.
174-
taskChild = spawnPendingTask(
182+
taskChild = createCacheNodeOnNavigation(
175183
newRouterStateChild,
176184
prefetchDataChild !== undefined ? prefetchDataChild : null,
177-
prefetchHead
185+
prefetchHead,
186+
isPrefetchHeadPartial
178187
)
179188
}
180189
} else {
181190
// This is a new tree. Switch to the "create" path.
182-
taskChild = spawnPendingTask(
191+
taskChild = createCacheNodeOnNavigation(
183192
newRouterStateChild,
184193
prefetchDataChild !== undefined ? prefetchDataChild : null,
185-
prefetchHead
194+
prefetchHead,
195+
isPrefetchHeadPartial
186196
)
187197
}
188198

@@ -197,8 +207,9 @@ export function updateCacheNodeOnNavigation(
197207
const newSegmentMapChild: ChildSegmentMap = new Map(oldSegmentMapChild)
198208
newSegmentMapChild.set(newSegmentKeyChild, newCacheNodeChild)
199209
prefetchParallelRoutes.set(parallelRouteKey, newSegmentMapChild)
200-
// a non-null taskChild.node means we're waiting for a dynamic request to
201-
// fill in the missing data
210+
}
211+
212+
if (taskChild.needsDynamicRequest) {
202213
needsDynamicRequest = true
203214
}
204215

@@ -241,9 +252,110 @@ export function updateCacheNodeOnNavigation(
241252
newRouterState,
242253
patchedRouterStateChildren
243254
),
244-
// Only return the new cache node if there are pending tasks that need to be resolved
245-
// by the dynamic data from the server. If they don't, we don't need to trigger a dynamic request.
246-
node: needsDynamicRequest ? newCacheNode : null,
255+
node: newCacheNode,
256+
needsDynamicRequest,
257+
children: taskChildren,
258+
}
259+
}
260+
261+
function createCacheNodeOnNavigation(
262+
routerState: FlightRouterState,
263+
prefetchData: CacheNodeSeedData | null,
264+
possiblyPartialPrefetchHead: React.ReactNode | null,
265+
isPrefetchHeadPartial: boolean
266+
): Task {
267+
// Same traversal as updateCacheNodeNavigation, but we switch to this path
268+
// once we reach the part of the tree that was not in the previous route. We
269+
// don't need to diff against the old tree, we just need to create a new one.
270+
if (prefetchData === null) {
271+
// There's no prefetch for this segment. Everything from this point will be
272+
// requested from the server, even if there are static children below it.
273+
// Create a terminal task node that will later be fulfilled by
274+
// server response.
275+
return spawnPendingTask(
276+
routerState,
277+
null,
278+
possiblyPartialPrefetchHead,
279+
isPrefetchHeadPartial
280+
)
281+
}
282+
283+
const routerStateChildren = routerState[1]
284+
const isPrefetchRscPartial = prefetchData[4]
285+
286+
// The head is assigned to every leaf segment delivered by the server. Based
287+
// on corresponding logic in fill-lazy-items-till-leaf-with-head.ts
288+
const isLeafSegment = Object.keys(routerStateChildren).length === 0
289+
290+
// If prefetch data is available for a segment, and it's fully static (i.e.
291+
// does not contain any dynamic holes), we don't need to request it from
292+
// the server.
293+
if (
294+
// Check if the segment data is partial
295+
isPrefetchRscPartial ||
296+
// Check if the head is partial (only relevant if this is a leaf segment)
297+
(isPrefetchHeadPartial && isLeafSegment)
298+
) {
299+
// We only have partial data from this segment. Like missing segments, we
300+
// must request the full data from the server.
301+
return spawnPendingTask(
302+
routerState,
303+
prefetchData,
304+
possiblyPartialPrefetchHead,
305+
isPrefetchHeadPartial
306+
)
307+
}
308+
309+
// The prefetched segment is fully static, so we don't need to request a new
310+
// one from the server. Keep traversing down the tree until we reach something
311+
// that requires a dynamic request.
312+
const prefetchDataChildren = prefetchData[2]
313+
const taskChildren = new Map()
314+
const cacheNodeChildren = new Map()
315+
let needsDynamicRequest = false
316+
for (let parallelRouteKey in routerStateChildren) {
317+
const routerStateChild: FlightRouterState =
318+
routerStateChildren[parallelRouteKey]
319+
const prefetchDataChild: CacheNodeSeedData | void | null =
320+
prefetchDataChildren !== null
321+
? prefetchDataChildren[parallelRouteKey]
322+
: null
323+
const segmentChild = routerStateChild[0]
324+
const segmentKeyChild = createRouterCacheKey(segmentChild)
325+
const taskChild = createCacheNodeOnNavigation(
326+
routerStateChild,
327+
prefetchDataChild,
328+
possiblyPartialPrefetchHead,
329+
isPrefetchHeadPartial
330+
)
331+
taskChildren.set(parallelRouteKey, taskChild)
332+
if (taskChild.needsDynamicRequest) {
333+
needsDynamicRequest = true
334+
}
335+
const newCacheNodeChild = taskChild.node
336+
if (newCacheNodeChild !== null) {
337+
const newSegmentMapChild: ChildSegmentMap = new Map()
338+
newSegmentMapChild.set(segmentKeyChild, newCacheNodeChild)
339+
cacheNodeChildren.set(parallelRouteKey, newSegmentMapChild)
340+
}
341+
}
342+
343+
const rsc = prefetchData[1]
344+
const loading = prefetchData[3]
345+
return {
346+
route: routerState,
347+
node: {
348+
lazyData: null,
349+
// Since this is a fully static segment, we don't need to use the
350+
// `prefetchRsc` field.
351+
rsc,
352+
prefetchRsc: null,
353+
head: isLeafSegment ? possiblyPartialPrefetchHead : null,
354+
prefetchHead: null,
355+
loading,
356+
parallelRoutes: cacheNodeChildren,
357+
},
358+
needsDynamicRequest,
247359
children: taskChildren,
248360
}
249361
}
@@ -271,19 +383,26 @@ function patchRouterStateWithNewChildren(
271383
function spawnPendingTask(
272384
routerState: FlightRouterState,
273385
prefetchData: CacheNodeSeedData | null,
274-
prefetchHead: React.ReactNode | null
386+
prefetchHead: React.ReactNode | null,
387+
isPrefetchHeadPartial: boolean
275388
): Task {
276389
// Create a task that will later be fulfilled by data from the server.
277-
const pendingCacheNode = createPendingCacheNode(
278-
routerState,
279-
prefetchData,
280-
prefetchHead
281-
)
282-
return {
390+
const newTask: Task = {
283391
route: routerState,
284-
node: pendingCacheNode,
392+
393+
// Corresponds to the part of the route that will be rendered on the server.
394+
node: createPendingCacheNode(
395+
routerState,
396+
prefetchData,
397+
prefetchHead,
398+
isPrefetchHeadPartial
399+
),
400+
// Set this to true to indicate that this tree is missing data. This will
401+
// be propagated to all the parent tasks.
402+
needsDynamicRequest: true,
285403
children: null,
286404
}
405+
return newTask
287406
}
288407

289408
function spawnReusedTask(reusedRouterState: FlightRouterState): Task {
@@ -292,6 +411,7 @@ function spawnReusedTask(reusedRouterState: FlightRouterState): Task {
292411
return {
293412
route: reusedRouterState,
294413
node: null,
414+
needsDynamicRequest: false,
295415
children: null,
296416
}
297417
}
@@ -413,6 +533,11 @@ function finishTaskUsingDynamicDataPayload(
413533
dynamicData: CacheNodeSeedData,
414534
dynamicHead: React.ReactNode
415535
) {
536+
if (!task.needsDynamicRequest) {
537+
// Everything in this subtree is already complete. Bail out.
538+
return
539+
}
540+
416541
// dynamicData may represent a larger subtree than the task. Before we can
417542
// finish the task, we need to line them up.
418543
const taskChildren = task.children
@@ -429,8 +554,8 @@ function finishTaskUsingDynamicDataPayload(
429554
dynamicData,
430555
dynamicHead
431556
)
432-
// Null this out to indicate that the task is complete.
433-
task.node = null
557+
// Set this to false to indicate that this task is now complete.
558+
task.needsDynamicRequest = false
434559
}
435560
return
436561
}
@@ -472,7 +597,8 @@ function finishTaskUsingDynamicDataPayload(
472597
function createPendingCacheNode(
473598
routerState: FlightRouterState,
474599
prefetchData: CacheNodeSeedData | null,
475-
prefetchHead: React.ReactNode | null
600+
prefetchHead: React.ReactNode | null,
601+
isPrefetchHeadPartial: boolean
476602
): ReadyCacheNode {
477603
const routerStateChildren = routerState[1]
478604
const prefetchDataChildren = prefetchData !== null ? prefetchData[2] : null
@@ -492,7 +618,8 @@ function createPendingCacheNode(
492618
const newCacheNodeChild = createPendingCacheNode(
493619
routerStateChild,
494620
prefetchDataChild === undefined ? null : prefetchDataChild,
495-
prefetchHead
621+
prefetchHead,
622+
isPrefetchHeadPartial
496623
)
497624

498625
const newSegmentMapChild: ChildSegmentMap = new Map()
@@ -503,7 +630,6 @@ function createPendingCacheNode(
503630
// The head is assigned to every leaf segment delivered by the server. Based
504631
// on corresponding logic in fill-lazy-items-till-leaf-with-head.ts
505632
const isLeafSegment = parallelRoutes.size === 0
506-
507633
const maybePrefetchRsc = prefetchData !== null ? prefetchData[1] : null
508634
const maybePrefetchLoading = prefetchData !== null ? prefetchData[3] : null
509635
return {
@@ -512,6 +638,10 @@ function createPendingCacheNode(
512638

513639
prefetchRsc: maybePrefetchRsc !== undefined ? maybePrefetchRsc : null,
514640
prefetchHead: isLeafSegment ? prefetchHead : null,
641+
642+
// TODO: Technically, a loading boundary could contain dynamic data. We must
643+
// have separate `loading` and `prefetchLoading` fields to handle this, like
644+
// we do for the segment data and head.
515645
loading: maybePrefetchLoading !== undefined ? maybePrefetchLoading : null,
516646

517647
// Create a deferred promise. This will be fulfilled once the dynamic
@@ -645,8 +775,8 @@ export function abortTask(task: Task, error: any): void {
645775
}
646776
}
647777

648-
// Null this out to indicate that the task is complete.
649-
task.node = null
778+
// Set this to false to indicate that this task is now complete.
779+
task.needsDynamicRequest = false
650780
}
651781

652782
function abortPendingCacheNode(

0 commit comments

Comments
 (0)