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

Add lazy initialize of router cache nodes #42629

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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 6 additions & 1 deletion packages/next/client/components/app-router.tsx
Expand Up @@ -7,6 +7,7 @@ import {
AppRouterContext,
LayoutRouterContext,
GlobalLayoutRouterContext,
CacheStates,
} from '../../shared/lib/app-router-context'
import type {
CacheNode,
Expand Down Expand Up @@ -121,11 +122,12 @@ function Router({
return {
tree: initialTree,
cache: {
status: CacheStates.READY,
data: null,
subTreeData: children,
parallelRoutes:
typeof window === 'undefined' ? new Map() : initialParallelRoutes,
},
} as CacheNode,
prefetchCache: new Map(),
pushRef: { pendingPush: false, mpaNavigation: false },
focusAndScrollRef: { apply: false },
Expand Down Expand Up @@ -176,6 +178,7 @@ function Router({
previousTree,
overrideCanonicalUrl,
cache: {
status: CacheStates.LAZYINITIALIZED,
data: null,
subTreeData: null,
parallelRoutes: new Map(),
Expand All @@ -201,6 +204,7 @@ function Router({
forceOptimisticNavigation,
navigateType,
cache: {
status: CacheStates.LAZYINITIALIZED,
data: null,
subTreeData: null,
parallelRoutes: new Map(),
Expand Down Expand Up @@ -262,6 +266,7 @@ function Router({

// TODO-APP: revisit if this needs to be passed.
cache: {
status: CacheStates.LAZYINITIALIZED,
data: null,
subTreeData: null,
parallelRoutes: new Map(),
Expand Down
38 changes: 24 additions & 14 deletions packages/next/client/components/layout-router.tsx
Expand Up @@ -16,6 +16,7 @@ import type {
} from '../../server/app-render'
import type { ErrorComponent } from './error-boundary'
import {
CacheStates,
LayoutRouterContext,
GlobalLayoutRouterContext,
TemplateContext,
Expand Down Expand Up @@ -143,21 +144,29 @@ export function InnerLayoutRouter({
if (
childProp &&
// TODO-APP: verify if this can be null based on user code
childProp.current !== null &&
!childNode /*&&
!childProp.partial*/
childProp.current !== null
) {
// Add the segment's subTreeData to the cache.
// This writes to the cache when there is no item in the cache yet. It never *overwrites* existing cache items which is why it's safe in concurrent mode.
childNodes.set(path, {
data: null,
subTreeData: childProp.current,
parallelRoutes: new Map(),
})
// Mutates the prop in order to clean up the memory associated with the subTreeData as it is now part of the cache.
childProp.current = null
// In the above case childNode was set on childNodes, so we have to get it from the cacheNodes again.
childNode = childNodes.get(path)
if (childNode && childNode.status === CacheStates.LAZYINITIALIZED) {
// @ts-expect-error TODO-APP: handle changing of the type
childNode.status = CacheStates.READY
// @ts-expect-error TODO-APP: handle changing of the type
childNode.subTreeData = childProp.current
// Mutates the prop in order to clean up the memory associated with the subTreeData as it is now part of the cache.
childProp.current = null
} else {
// Add the segment's subTreeData to the cache.
// This writes to the cache when there is no item in the cache yet. It never *overwrites* existing cache items which is why it's safe in concurrent mode.
childNodes.set(path, {
status: CacheStates.READY,
data: null,
subTreeData: childProp.current,
parallelRoutes: new Map(),
})
// Mutates the prop in order to clean up the memory associated with the subTreeData as it is now part of the cache.
childProp.current = null
// In the above case childNode was set on childNodes, so we have to get it from the cacheNodes again.
childNode = childNodes.get(path)
}
}

// When childNode is not available during rendering client-side we need to fetch it from the server.
Expand All @@ -172,6 +181,7 @@ export function InnerLayoutRouter({
* Flight data fetch kicked off during render and put into the cache.
*/
childNodes.set(path, {
status: CacheStates.DATAFETCH,
data: fetchServerResponse(new URL(url, location.origin), refetchTree),
subTreeData: null,
parallelRoutes: new Map(),
Expand Down
83 changes: 78 additions & 5 deletions packages/next/client/components/reducer.ts
@@ -1,4 +1,4 @@
import type { CacheNode } from '../../shared/lib/app-router-context'
import { CacheNode, CacheStates } from '../../shared/lib/app-router-context'
import type {
FlightRouterState,
FlightData,
Expand Down Expand Up @@ -55,7 +55,7 @@ function invalidateCacheByRouterState(
newCache: CacheNode,
existingCache: CacheNode,
routerState: FlightRouterState
) {
): void {
// Remove segment that we got data for so that it is filled in during rendering of subTreeData.
for (const key in routerState[1]) {
const segmentForParallelRoute = routerState[1][key][0]
Expand All @@ -72,6 +72,65 @@ function invalidateCacheByRouterState(
}
}

function fillLazyItemsTillLeafWithHead(
newCache: CacheNode,
existingCache: CacheNode | undefined,
routerState: FlightRouterState,
head: React.ReactNode
): void {
const isLastSegment = Object.keys(routerState[1]).length === 0
if (isLastSegment) {
newCache.head = head
return
}
// Remove segment that we got data for so that it is filled in during rendering of subTreeData.
for (const key in routerState[1]) {
const parallelRouteState = routerState[1][key]
const segmentForParallelRoute = parallelRouteState[0]
const cacheKey = Array.isArray(segmentForParallelRoute)
? segmentForParallelRoute[1]
: segmentForParallelRoute
if (existingCache) {
const existingParallelRoutesCacheNode =
existingCache.parallelRoutes.get(key)
if (existingParallelRoutesCacheNode) {
let parallelRouteCacheNode = new Map(existingParallelRoutesCacheNode)
parallelRouteCacheNode.delete(cacheKey)
const newCacheNode: CacheNode = {
status: CacheStates.LAZYINITIALIZED,
data: null,
subTreeData: null,
parallelRoutes: new Map(),
}
parallelRouteCacheNode.set(cacheKey, newCacheNode)
fillLazyItemsTillLeafWithHead(
newCacheNode,
undefined,
parallelRouteState,
head
)

newCache.parallelRoutes.set(key, parallelRouteCacheNode)
continue
}
}

const newCacheNode: CacheNode = {
status: CacheStates.LAZYINITIALIZED,
data: null,
subTreeData: null,
parallelRoutes: new Map(),
}
newCache.parallelRoutes.set(key, new Map([[cacheKey, newCacheNode]]))
fillLazyItemsTillLeafWithHead(
newCacheNode,
undefined,
parallelRouteState,
head
)
}
}

/**
* Fill cache with subTreeData based on flightDataPath
*/
Expand Down Expand Up @@ -111,6 +170,7 @@ function fillCacheWithNewSubTreeData(
childCacheNode === existingChildCacheNode
) {
childCacheNode = {
status: CacheStates.READY,
data: null,
subTreeData: flightDataPath[3],
// Ensure segments other than the one we got data for are preserved.
Expand All @@ -127,6 +187,13 @@ function fillCacheWithNewSubTreeData(
)
}

fillLazyItemsTillLeafWithHead(
childCacheNode,
existingChildCacheNode,
flightDataPath[2],
/* flightDataPath[4] */ undefined
)

childSegmentMap.set(segmentForCache, childCacheNode)
}
return
Expand All @@ -140,10 +207,11 @@ function fillCacheWithNewSubTreeData(

if (childCacheNode === existingChildCacheNode) {
childCacheNode = {
status: childCacheNode.status,
data: childCacheNode.data,
subTreeData: childCacheNode.subTreeData,
parallelRoutes: new Map(childCacheNode.parallelRoutes),
}
} as CacheNode
childSegmentMap.set(segmentForCache, childCacheNode)
}

Expand Down Expand Up @@ -199,10 +267,11 @@ function invalidateCacheBelowFlightSegmentPath(

if (childCacheNode === existingChildCacheNode) {
childCacheNode = {
status: childCacheNode.status,
data: childCacheNode.data,
subTreeData: childCacheNode.subTreeData,
parallelRoutes: new Map(childCacheNode.parallelRoutes),
}
} as CacheNode
childSegmentMap.set(segmentForCache, childCacheNode)
}

Expand Down Expand Up @@ -239,6 +308,7 @@ function fillCacheWithPrefetchedSubTreeData(
if (isLastEntry) {
if (!existingChildCacheNode) {
existingChildSegmentMap.set(segmentForCache, {
status: CacheStates.READY,
data: null,
subTreeData: flightDataPath[3],
parallelRoutes: new Map(),
Expand Down Expand Up @@ -300,6 +370,7 @@ function fillCacheWithDataProperty(
childCacheNode === existingChildCacheNode
) {
childSegmentMap.set(segment, {
status: CacheStates.DATAFETCH,
data: fetchResponse(),
subTreeData: null,
parallelRoutes: new Map(),
Expand All @@ -312,6 +383,7 @@ function fillCacheWithDataProperty(
// Start fetch in the place where the existing cache doesn't have the data yet.
if (!childCacheNode) {
childSegmentMap.set(segment, {
status: CacheStates.DATAFETCH,
data: fetchResponse(),
subTreeData: null,
parallelRoutes: new Map(),
Expand All @@ -322,10 +394,11 @@ function fillCacheWithDataProperty(

if (childCacheNode === existingChildCacheNode) {
childCacheNode = {
status: childCacheNode.status,
data: childCacheNode.data,
subTreeData: childCacheNode.subTreeData,
parallelRoutes: new Map(childCacheNode.parallelRoutes),
}
} as CacheNode
childSegmentMap.set(segment, childCacheNode)
}

Expand Down
69 changes: 52 additions & 17 deletions packages/next/shared/lib/app-router-context.ts
Expand Up @@ -6,26 +6,61 @@ import type { FlightRouterState, FlightData } from '../../server/app-render'

export type ChildSegmentMap = Map<string, CacheNode>

// eslint-disable-next-line no-shadow
export enum CacheStates {
LAZYINITIALIZED = 'LAZYINITIALIZED',
DATAFETCH = 'DATAFETCH',
READY = 'READY',
}

/**
* Cache node used in app-router / layout-router.
*/
export type CacheNode = {
/**
* In-flight request for this node.
*/
data: ReturnType<
typeof import('../../client/components/app-router').fetchServerResponse
> | null
/**
* React Component for this node.
*/
subTreeData: React.ReactNode | null
/**
* Child parallel routes.
*/
parallelRoutes: Map<string, ChildSegmentMap>
}

export type CacheNode =
| {
status: CacheStates.DATAFETCH
/**
* In-flight request for this node.
*/
data: ReturnType<
typeof import('../../client/components/app-router').fetchServerResponse
> | null
head?: React.ReactNode
/**
* React Component for this node.
*/
subTreeData: null
/**
* Child parallel routes.
*/
parallelRoutes: Map<string, ChildSegmentMap>
}
| {
status: CacheStates.READY
/**
* In-flight request for this node.
*/
data: null
head?: React.ReactNode
/**
* React Component for this node.
*/
subTreeData: React.ReactNode
/**
* Child parallel routes.
*/
parallelRoutes: Map<string, ChildSegmentMap>
}
| {
status: CacheStates.LAZYINITIALIZED
data: null
head?: React.ReactNode
subTreeData: null
/**
* Child parallel routes.
*/
parallelRoutes: Map<string, ChildSegmentMap>
}
interface NavigateOptions {
forceOptimisticNavigation?: boolean
}
Expand Down