Skip to content

Commit

Permalink
[Segment Cache] Add isPartial to segment prefetch
Browse files Browse the repository at this point in the history
During a navigation, we should be able to skip the dynamic request if
the prefetched data does not contain any dynamic holes. In the previous
cache implementation, we only tracked this per route; in the Segment
Cache, we must track this per segment.

This updates the SegmentCacheEntry and CacheNodeSeedData types to
include an `isPartial` field. The field is always false during a dynamic
render, or when PPR is disabled.

No behavior changes are included in this PR.
  • Loading branch information
acdlite committed Dec 6, 2024
1 parent 144fd73 commit 75f0518
Show file tree
Hide file tree
Showing 8 changed files with 72 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const getFlightData = (): NormalizedFlightData[] => {
segmentPath: ['children', 'linking', 'children', 'about'],
segment: 'about',
tree: ['about', { children: ['', {}] }],
seedData: ['about', <h1>SubTreeData Injected!</h1>, {}, null],
seedData: ['about', <h1>SubTreeData Injected!</h1>, {}, null, false],
head: '<title>Head Injected!</title>',
isRootRender: false,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const getFlightData = (): NormalizedFlightData[] => {
segmentPath: ['children', 'linking', 'children', 'about'],
segment: 'about',
tree: ['about', { children: ['', {}] }],
seedData: ['about', <h1>About Page!</h1>, {}, null],
seedData: ['about', <h1>About Page!</h1>, {}, null, false],
head: '<title>About page!</title>',
isRootRender: false,
},
Expand Down
11 changes: 9 additions & 2 deletions packages/next/src/client/components/segment-cache/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,20 +117,23 @@ type PendingSegmentCacheEntry = SegmentCacheEntryShared & {
status: EntryStatus.Pending
rsc: null
loading: null
isPartial: true
promise: null | PromiseWithResolvers<FulfilledSegmentCacheEntry | null>
}

type RejectedSegmentCacheEntry = SegmentCacheEntryShared & {
status: EntryStatus.Rejected
rsc: null
loading: null
isPartial: true
promise: null
}

type FulfilledSegmentCacheEntry = SegmentCacheEntryShared & {
status: EntryStatus.Fulfilled
rsc: React.ReactNode | null
loading: LoadingModuleData | Promise<LoadingModuleData>
isPartial: boolean
promise: null
}

Expand Down Expand Up @@ -328,6 +331,7 @@ export function requestSegmentEntryFromCache(
rsc: null,
loading: null,
staleAt: route.staleAt,
isPartial: true,
promise: null,

// LRU-related fields
Expand Down Expand Up @@ -435,13 +439,15 @@ function fulfillSegmentCacheEntry(
segmentCacheEntry: PendingSegmentCacheEntry,
rsc: React.ReactNode,
loading: LoadingModuleData | Promise<LoadingModuleData>,
staleAt: number
staleAt: number,
isPartial: boolean
) {
const fulfilledEntry: FulfilledSegmentCacheEntry = segmentCacheEntry as any
fulfilledEntry.status = EntryStatus.Fulfilled
fulfilledEntry.rsc = rsc
fulfilledEntry.loading = loading
fulfilledEntry.staleAt = staleAt
fulfilledEntry.isPartial = isPartial
// Resolve any listeners that were waiting for this data.
if (segmentCacheEntry.promise !== null) {
segmentCacheEntry.promise.resolve(fulfilledEntry)
Expand Down Expand Up @@ -612,7 +618,8 @@ async function fetchSegmentEntryOnCacheMiss(
serverData.loading,
// TODO: The server does not currently provide per-segment stale time.
// So we use the stale time of the route.
route.staleAt
route.staleAt,
serverData.isPartial
)
} catch (error) {
// Either the connection itself failed, or something bad happened while
Expand Down
14 changes: 13 additions & 1 deletion packages/next/src/client/components/segment-cache/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ function readRenderSnapshotFromCache(

let rsc: React.ReactNode | null = null
let loading: LoadingModuleData | Promise<LoadingModuleData> = null
let isPartial: boolean = true

const segmentEntry = readSegmentCacheEntry(now, tree.path)
if (segmentEntry !== null) {
Expand All @@ -189,6 +190,7 @@ function readRenderSnapshotFromCache(
// Happy path: a cache hit
rsc = segmentEntry.rsc
loading = segmentEntry.loading
isPartial = segmentEntry.isPartial
break
}
case EntryStatus.Pending: {
Expand All @@ -202,6 +204,10 @@ function readRenderSnapshotFromCache(
loading = promiseForFulfilledEntry.then((entry) =>
entry !== null ? entry.loading : null
)
// Since we don't know yet whether the segment is partial or fully
// static, we must assume it's partial; we can't skip the
// dynamic request.
isPartial = true
break
}
case EntryStatus.Rejected:
Expand All @@ -225,7 +231,13 @@ function readRenderSnapshotFromCache(
null,
isRootLayout,
],
seedData: [flightRouterStateSegment, rsc, childSeedDatas, loading],
seedData: [
flightRouterStateSegment,
rsc,
childSeedDatas,
loading,
isPartial,
],
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/next/src/server/app-render/app-render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -872,6 +872,7 @@ async function getErrorRSCPayload(
</html>,
{},
null,
false,
]

const globalErrorStyles = await getGlobalErrorStyles(tree, ctx)
Expand Down
26 changes: 26 additions & 0 deletions packages/next/src/server/app-render/collect-segment-data.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export type SegmentPrefetch = {
buildId: string
rsc: React.ReactNode | null
loading: LoadingModuleData | Promise<LoadingModuleData>
isPartial: boolean
}

export async function collectSegmentData(
Expand Down Expand Up @@ -308,6 +309,7 @@ async function renderSegmentPrefetch(
buildId,
rsc,
loading,
isPartial: 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 Down Expand Up @@ -342,6 +344,30 @@ async function renderSegmentPrefetch(
}
}

async function isPartialRSCData(
rsc: React.ReactNode,
clientModules: ManifestNode
): Promise<boolean> {
// We can determine if a segment contains only partial data if it takes longer
// than a task to encode, because dynamic data is encoded as an infinite
// promise. We must do this in a separate Flight prerender from the one that
// actually generates the prefetch stream because we need to include
// `isPartial` in the stream itself.
let isPartial = false
const abortController = new AbortController()
waitAtLeastOneReactRenderTask().then(() => {
// If we haven't yet finished the outer task, then it must be because we
// accessed dynamic data.
isPartial = true
abortController.abort()
})
await prerender(rsc, clientModules, {
signal: abortController.signal,
onError() {},
})
return isPartial
}

// TODO: Consider updating or unifying this encoding logic for segments with
// createRouterCacheKey on the client, perhaps by including it as part of
// the FlightRouterState. Theoretically the client should never have to do its
Expand Down
20 changes: 20 additions & 0 deletions packages/next/src/server/app-render/create-component-tree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,22 @@ async function createComponentTreeInternal({

const isStaticGeneration = workStore.isStaticGeneration

// Assume the segment we're rendering contains only partial data if PPR is
// enabled and this is a statically generated response. This is used by the
// client Segment Cache after a prefetch to determine if it can skip the
// second request to fill in the dynamic data.
//
// It's OK for this to be `true` when the data is actually fully static, but
// it's not OK for this to be `false` when the data possibly contains holes.
// Although the value here is overly pessimistic, for prefetches, it will be
// replaced by a more specific value when the data is later processed into
// per-segment responses (see collect-segment-data.tsx)
//
// For dynamic requests, this must always be `false` because dynamic responses
// are never partial.
const isPossiblyPartialResponse =
isStaticGeneration && experimental.isRoutePPREnabled === true

// If there's a dynamic usage error attached to the store, throw it.
if (workStore.dynamicUsageErr) {
throw workStore.dynamicUsageErr
Expand Down Expand Up @@ -548,6 +564,7 @@ async function createComponentTreeInternal({
</React.Fragment>,
parallelRouteCacheNodeSeedData,
loadingData,
isPossiblyPartialResponse,
]
}

Expand Down Expand Up @@ -580,6 +597,7 @@ async function createComponentTreeInternal({
</React.Fragment>,
parallelRouteCacheNodeSeedData,
loadingData,
true,
]
}

Expand Down Expand Up @@ -650,6 +668,7 @@ async function createComponentTreeInternal({
</React.Fragment>,
parallelRouteCacheNodeSeedData,
loadingData,
isPossiblyPartialResponse,
]
} else {
const SegmentComponent = Component
Expand Down Expand Up @@ -829,6 +848,7 @@ async function createComponentTreeInternal({
segmentNode,
parallelRouteCacheNodeSeedData,
loadingData,
isPossiblyPartialResponse,
]
}
}
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 @@ -97,6 +97,7 @@ export type CacheNodeSeedData = [
[parallelRouterKey: string]: CacheNodeSeedData | null
},
loading: LoadingModuleData | Promise<LoadingModuleData>,
isPartial: boolean,
]

export type FlightDataSegment = [
Expand Down

0 comments on commit 75f0518

Please sign in to comment.