Skip to content

Commit

Permalink
refactor: collectAppPageSegments (#73908)
Browse files Browse the repository at this point in the history
This refactors `collectAppPageSegments` to be more efficient especially
when dealing with multiple parallel routes, where the number of
combinations of segments can drastically increase how many segments are
returned per route. Previously this function only considered the
`children` parallel route. When it was updated to consider all possible
parallel routes in #73358, this had the unintended side effect of
increasing the amount of duplicate segments this function would have to
process.

We only need to return unique segments discovered from a particular
loader tree (representing a page), as opposed to the same segment as
many times as it might appear across all parallel routes. This PR uses a
map to track unique segments with a composite key used to identify the
discovered segments.

Additionally, this refactors the function to be iterative rather than
recursive. We keep track of a queue of segments to be processed (a tuple
of `[loaderTree, segments]`), and as we discover new parallel route
paths, we process the queue further.

Fixes #73793
Closes #73871 (h/t to @icyJoseph for identifying the cause of the memory
consumption)
  • Loading branch information
ztanner authored Dec 13, 2024
1 parent c2e7c89 commit 2889713
Showing 1 changed file with 37 additions and 20 deletions.
57 changes: 37 additions & 20 deletions packages/next/src/build/segment-config/app/app-segments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ import {
} from '../../../server/route-modules/checks'
import { isClientReference } from '../../../lib/client-reference'
import { getSegmentParam } from '../../../server/app-render/get-segment-param'
import { getLayoutOrPageModule } from '../../../server/lib/app-dir-module'
import {
getLayoutOrPageModule,
type LoaderTree,
} from '../../../server/lib/app-dir-module'
import { PAGE_SEGMENT_KEY } from '../../../shared/lib/segment'

type GenerateStaticParams = (options: { params?: Params }) => Promise<Params[]>
Expand Down Expand Up @@ -74,17 +77,21 @@ export type AppSegment = {
* @returns the segments for the app page route module
*/
async function collectAppPageSegments(routeModule: AppPageRouteModule) {
const segments: AppSegment[] = []
// We keep track of unique segments, since with parallel routes, it's possible
// to see the same segment multiple times.
const uniqueSegments = new Map<string, AppSegment>()

// Queue will store tuples of [loaderTree, currentSegments]
type QueueItem = [LoaderTree, AppSegment[]]
const queue: QueueItem[] = [[routeModule.userland.loaderTree, []]]

// Helper function to process a loader tree path
async function processLoaderTree(
loaderTree: any,
currentSegments: AppSegment[] = []
): Promise<void> {
while (queue.length > 0) {
const [loaderTree, currentSegments] = queue.shift()!
const [name, parallelRoutes] = loaderTree
const { mod: userland, filePath } = await getLayoutOrPageModule(loaderTree)

const isClientComponent: boolean = userland && isClientReference(userland)
// Process current node
const { mod: userland, filePath } = await getLayoutOrPageModule(loaderTree)
const isClientComponent = userland && isClientReference(userland)
const isDynamicSegment = /\[.*\]$/.test(name)
const param = isDynamicSegment ? getSegmentParam(name)?.param : undefined

Expand All @@ -97,30 +104,40 @@ async function collectAppPageSegments(routeModule: AppPageRouteModule) {
generateStaticParams: undefined,
}

// Only server components can have app segment configurations. If this isn't
// an object, then we should skip it. This can happen when parsing the
// error components.
// Only server components can have app segment configurations
if (!isClientComponent) {
attach(segment, userland, routeModule.definition.pathname)
}

currentSegments.push(segment)
// Create a unique key for the segment
const segmentKey = getSegmentKey(segment)
if (!uniqueSegments.has(segmentKey)) {
uniqueSegments.set(segmentKey, segment)
}

// If this is a page segment, we know we've reached a leaf node associated with the
// page we're collecting segments for. We can add the collected segments to our final result.
const updatedSegments = [...currentSegments, segment]

// If this is a page segment, we've reached a leaf node
if (name === PAGE_SEGMENT_KEY) {
segments.push(...currentSegments)
// Add all segments in the current path
updatedSegments.forEach((seg) => {
const key = getSegmentKey(seg)
uniqueSegments.set(key, seg)
})
}

// Recursively process parallel routes
// Add all parallel routes to the queue
for (const parallelRouteKey in parallelRoutes) {
const parallelRoute = parallelRoutes[parallelRouteKey]
await processLoaderTree(parallelRoute, [...currentSegments])
queue.push([parallelRoute, updatedSegments])
}
}

await processLoaderTree(routeModule.userland.loaderTree)
return segments
return Array.from(uniqueSegments.values())
}

function getSegmentKey(segment: AppSegment) {
return `${segment.name}-${segment.filePath ?? ''}-${segment.param ?? ''}`
}

/**
Expand Down

0 comments on commit 2889713

Please sign in to comment.