From fbf91745e171a78459ca93287f5f8f7fc9dbcf46 Mon Sep 17 00:00:00 2001 From: Nathan Nguyen <146415969+NathanDrake2406@users.noreply.github.com> Date: Fri, 19 Jun 2026 23:51:05 +1000 Subject: [PATCH 1/4] perf(server): cache App Router route wiring plans App Router SSR repeatedly rebuilds route-invariant layout, template, error, and slot wiring metadata while constructing each page response. That work is unnecessary for generated route objects that are stable for the lifetime of the server process. Cache the derived wiring plan per route object and keep per-request work focused on params, dependency barriers, and React element construction. Also avoid template and slot dependency allocations on routes that do not use those features. --- .../src/server/app-page-route-wiring.tsx | 315 ++++++++++++------ 1 file changed, 219 insertions(+), 96 deletions(-) diff --git a/packages/vinext/src/server/app-page-route-wiring.tsx b/packages/vinext/src/server/app-page-route-wiring.tsx index 48ff4731f..5dc0943a8 100644 --- a/packages/vinext/src/server/app-page-route-wiring.tsx +++ b/packages/vinext/src/server/app-page-route-wiring.tsx @@ -3,7 +3,6 @@ import { AppElementsWire, APP_PREFETCH_LOADING_SHELL_MARKER_KEY, APP_STATIC_SIBLINGS_KEY, - normalizeAppElementsSlotBindings, type AppElements, type AppElementsInterception, type AppElementsSlotBinding, @@ -69,6 +68,7 @@ type AppPageComponent = ComponentType; type AppPageErrorComponent = ComponentType<{ error: unknown; reset: () => void }>; const APP_PAGE_LAYOUT_PROBE_CHILD = ; const DEFAULT_GLOBAL_ERROR_COMPONENT = DefaultGlobalError as AppPageErrorComponent; +const EMPTY_APP_RENDER_DEPENDENCIES: readonly AppRenderDependency[] = []; export type AppPageModule = Record & { default?: AppPageComponent | null | undefined; @@ -220,6 +220,36 @@ type AppPageErrorEntry = readonly [string, AppPageRouteWiringSlot]; + +type AppPageRouteWiringPlan< + TModule extends AppPageModule = AppPageModule, + TErrorModule extends AppPageErrorModule = AppPageErrorModule, +> = { + defaultSourcePage: string; + errorEntries: readonly AppPageErrorEntry[]; + errorEntriesByTreePosition: ReadonlyMap>; + layoutEntries: readonly AppPageLayoutEntry[]; + layoutEntriesByTreePosition: ReadonlyMap>; + layoutIds: readonly string[]; + layoutIndicesByTreePosition: ReadonlyMap; + orderedTreePositions: readonly number[]; + rootLayoutTreePath: string | null; + routeSegments: readonly string[]; + slotEntries: readonly AppPageSlotEntry[]; + slotNameCounts: ReadonlyMap | null; + templateEntries: readonly AppPageTemplateEntry[]; + templateEntriesByTreePosition: ReadonlyMap>; +}; + +const appPageRouteWiringPlanCache = new WeakMap< + object, + AppPageRouteWiringPlan +>(); + function getDefaultExport( module: TModule | null | undefined, ): AppPageComponent | null { @@ -384,18 +414,99 @@ function createAppPageErrorEntries( }); } +function createAppPageRouteWiringPlan< + TModule extends AppPageModule, + TErrorModule extends AppPageErrorModule, +>( + route: AppPageRouteWiringRoute, +): AppPageRouteWiringPlan { + const routeSegments = route.routeSegments ?? []; + const layoutEntries = createAppPageLayoutEntries(route); + const templateEntries = createAppPageTemplateEntries(route); + const errorEntries = createAppPageErrorEntries(route); + const layoutEntriesByTreePosition = new Map>(); + const templateEntriesByTreePosition = new Map>(); + const errorEntriesByTreePosition = new Map>(); + const layoutIndicesByTreePosition = new Map(); + + for (let index = 0; index < layoutEntries.length; index++) { + const layoutEntry = layoutEntries[index]; + layoutEntriesByTreePosition.set(layoutEntry.treePosition, layoutEntry); + layoutIndicesByTreePosition.set(layoutEntry.treePosition, index); + } + for (const templateEntry of templateEntries) { + templateEntriesByTreePosition.set(templateEntry.treePosition, templateEntry); + } + for (const errorEntry of errorEntries) { + errorEntriesByTreePosition.set(errorEntry.treePosition, errorEntry); + } + + const slotEntries: AppPageSlotEntry[] = route.slots + ? Object.entries(route.slots) + : []; + let slotNameCounts: Map | null = null; + if (slotEntries.length > 0) { + slotNameCounts = new Map(); + for (const [, slot] of slotEntries) { + slotNameCounts.set(slot.name, (slotNameCounts.get(slot.name) ?? 0) + 1); + } + } + + return { + defaultSourcePage: createAppPageSourcePage(routeSegments), + errorEntries, + errorEntriesByTreePosition, + layoutEntries, + layoutEntriesByTreePosition, + layoutIds: route.ids?.layouts ?? layoutEntries.map((entry) => entry.id), + layoutIndicesByTreePosition, + orderedTreePositions: Array.from( + new Set([ + ...layoutEntries.map((entry) => entry.treePosition), + ...templateEntries.map((entry) => entry.treePosition), + ...errorEntries.map((entry) => entry.treePosition), + ]), + ).sort((left, right) => left - right), + rootLayoutTreePath: layoutEntries[0]?.treePath ?? null, + routeSegments, + slotEntries, + slotNameCounts, + templateEntries, + templateEntriesByTreePosition, + }; +} + +function getAppPageRouteWiringPlan< + TModule extends AppPageModule, + TErrorModule extends AppPageErrorModule, +>( + route: AppPageRouteWiringRoute, +): AppPageRouteWiringPlan { + const cached = appPageRouteWiringPlanCache.get(route); + if (cached) { + return cached as unknown as AppPageRouteWiringPlan; + } + + const plan = createAppPageRouteWiringPlan(route); + appPageRouteWiringPlanCache.set( + route, + plan as unknown as AppPageRouteWiringPlan, + ); + return plan; +} + function createAppPageParallelSlotEntries< TModule extends AppPageModule, TErrorModule extends AppPageErrorModule, >( layoutIndex: number, layoutEntries: readonly AppPageLayoutEntry[], - route: AppPageRouteWiringRoute, + slotEntries: readonly AppPageSlotEntry[], getEffectiveSlotParams: (slotKey: string, slotName: string) => AppPageParams, ): Readonly> | undefined { const parallelSlots: Record = {}; - for (const [slotKey, slot] of Object.entries(route.slots ?? {})) { + for (const [slotKey, slot] of slotEntries) { const slotName = slot.name; const targetIndex = slot.layoutIndex >= 0 ? slot.layoutIndex : layoutEntries.length - 1; if (targetIndex !== layoutIndex) { @@ -439,11 +550,17 @@ function resolveAppPageSlotBindingState( return "unmatched"; } +function resolveEmptyAppPageSlotOverride(): + | AppPageSlotOverride + | undefined { + return undefined; +} + function createAppPageSlotBindings< TModule extends AppPageModule, TErrorModule extends AppPageErrorModule, >( - route: AppPageRouteWiringRoute, + slotEntries: readonly AppPageSlotEntry[], layoutEntries: readonly AppPageLayoutEntry[], resolveSlotOverride: ( slotKey: string, @@ -456,7 +573,7 @@ function createAppPageSlotBindings< }, ): readonly AppElementsSlotBinding[] { const bindings: AppElementsSlotBinding[] = []; - for (const [slotKey, slot] of Object.entries(route.slots ?? {})) { + for (const [slotKey, slot] of slotEntries) { const targetIndex = slot.layoutIndex >= 0 ? slot.layoutIndex : layoutEntries.length - 1; const layoutEntry = layoutEntries[targetIndex] ?? null; const ownerLayoutId = layoutEntry?.id ?? null; @@ -476,9 +593,7 @@ function createAppPageSlotBindings< state, }); } - return normalizeAppElementsSlotBindings(bindings, { - layoutIds: layoutEntries.map((entry) => entry.id), - }); + return bindings; } function createAppPageRouteHead( @@ -524,67 +639,56 @@ export function buildAppPageElements< const interceptionContext = renderIdentity?.interceptionContext ?? options.interceptionContext ?? null; const renderMode = options.renderMode ?? APP_RSC_RENDER_MODE_NAVIGATION; - const routeSegments = options.route.routeSegments ?? []; + const routePlan = getAppPageRouteWiringPlan(options.route); + const routeSegments = routePlan.routeSegments; const routeResetKey = resolveAppPageRouteStateKey(routeSegments, options.matchedParams); const routeId = renderIdentity?.routeId ?? AppElementsWire.encodeRouteId(options.routePath, interceptionContext); const pageId = renderIdentity?.pageId ?? AppElementsWire.encodePageId(options.routePath, interceptionContext); - const layoutEntries = createAppPageLayoutEntries(options.route); - const templateEntries = createAppPageTemplateEntries(options.route); - const errorEntries = createAppPageErrorEntries(options.route); + const layoutEntries = routePlan.layoutEntries; + const templateEntries = routePlan.templateEntries; + const errorEntries = routePlan.errorEntries; const metadataPlacement = options.metadataPlacement ?? "head"; - const layoutEntriesByTreePosition = new Map>(); - const templateEntriesByTreePosition = new Map>(); - const errorEntriesByTreePosition = new Map>(); - for (const layoutEntry of layoutEntries) { - layoutEntriesByTreePosition.set(layoutEntry.treePosition, layoutEntry); - } - for (const templateEntry of templateEntries) { - templateEntriesByTreePosition.set(templateEntry.treePosition, templateEntry); - } - for (const errorEntry of errorEntries) { - errorEntriesByTreePosition.set(errorEntry.treePosition, errorEntry); - } - const layoutIndicesByTreePosition = new Map(); - for (let index = 0; index < layoutEntries.length; index++) { - layoutIndicesByTreePosition.set(layoutEntries[index].treePosition, index); - } - const layoutDependenciesByIndex = new Map(); + const layoutEntriesByTreePosition = routePlan.layoutEntriesByTreePosition; + const templateEntriesByTreePosition = routePlan.templateEntriesByTreePosition; + const errorEntriesByTreePosition = routePlan.errorEntriesByTreePosition; + const layoutIndicesByTreePosition = routePlan.layoutIndicesByTreePosition; + const slotEntries = routePlan.slotEntries; + const hasSlots = slotEntries.length > 0; + const hasTemplates = templateEntries.length > 0; + const layoutDependenciesByIndex: AppRenderDependency[] = []; const renderDependenciesByElementId = new Map(); - const layoutDependenciesBefore: AppRenderDependency[][] = []; - const slotDependenciesByLayoutIndex: AppRenderDependency[][] = []; - const templateDependenciesById = new Map(); - const templateDependenciesBeforeById = new Map(); + const layoutDependenciesBefore: Array = []; + const slotDependenciesByLayoutIndex: Array = []; + const templateDependenciesById = hasTemplates ? new Map() : null; + const templateDependenciesBeforeById = hasTemplates + ? new Map() + : null; const pageDependencies: AppRenderDependency[] = []; - const rootLayoutTreePath = layoutEntries[0]?.treePath ?? null; - const slotNameCounts = new Map(); - for (const slot of Object.values(options.route.slots ?? {})) { - const slotName = slot.name; - slotNameCounts.set(slotName, (slotNameCounts.get(slotName) ?? 0) + 1); - } - const orderedTreePositions = Array.from( - new Set([ - ...layoutEntries.map((entry) => entry.treePosition), - ...templateEntries.map((entry) => entry.treePosition), - ...errorEntries.map((entry) => entry.treePosition), - ]), - ).sort((left, right) => left - right); - const resolveSlotOverride = (slotKey: string, slotName: string) => { - const overrideByKey = options.slotOverrides?.[slotKey]; - if (overrideByKey) { - return overrideByKey; - } - - // Legacy callers may still provide overrides by slot prop name. - // Only allow that fallback when it is unambiguous. - if (slotKey === slotName || (slotNameCounts.get(slotName) ?? 0) === 1) { - return options.slotOverrides?.[slotName]; - } - - return undefined; - }; + const rootLayoutTreePath = routePlan.rootLayoutTreePath; + const slotNameCounts = routePlan.slotNameCounts; + const orderedTreePositions = routePlan.orderedTreePositions; + const resolveSlotOverride: ( + slotKey: string, + slotName: string, + ) => AppPageSlotOverride | undefined = hasSlots + ? (slotKey, slotName) => { + const overrideByKey = options.slotOverrides?.[slotKey]; + if (overrideByKey) { + return overrideByKey; + } + + // Legacy callers may still provide overrides by slot prop name. + // Only allow that fallback when it is unambiguous. + if (slotKey === slotName || (slotNameCounts?.get(slotName) ?? 0) === 1) { + return options.slotOverrides?.[slotName]; + } + + return undefined; + } + : resolveEmptyAppPageSlotOverride; const elements: Record< string, | ReactNode @@ -597,15 +701,19 @@ export function buildAppPageElements< ...AppElementsWire.createMetadataEntries({ interception: renderIdentity?.interception ?? options.interception ?? null, interceptionContext, - layoutIds: options.route.ids?.layouts ?? layoutEntries.map((entry) => entry.id), + layoutIds: routePlan.layoutIds, rootLayoutTreePath, routeId, - sourcePage: createAppPageSourcePage(options.sourcePageSegments ?? routeSegments), - slotBindings: createAppPageSlotBindings(options.route, layoutEntries, resolveSlotOverride, { - interception: renderIdentity?.interception ?? options.interception ?? null, - interceptionContext, - routePath: options.routePath, - }), + sourcePage: options.sourcePageSegments + ? createAppPageSourcePage(options.sourcePageSegments) + : routePlan.defaultSourcePage, + slotBindings: hasSlots + ? createAppPageSlotBindings(slotEntries, layoutEntries, resolveSlotOverride, { + interception: renderIdentity?.interception ?? options.interception ?? null, + interceptionContext, + routePath: options.routePath, + }) + : undefined, }), }; // Surface static-sibling info on the wire so the client router can decide @@ -618,31 +726,40 @@ export function buildAppPageElements< elements[APP_STATIC_SIBLINGS_KEY] = options.route.staticSiblings; } const getEffectiveSlotParams = (slotKey: string, slotName: string): AppPageParams => - resolveSlotOverride(slotKey, slotName)?.params ?? options.matchedParams; + resolveSlotOverride?.(slotKey, slotName)?.params ?? options.matchedParams; for (const treePosition of orderedTreePositions) { const layoutIndex = layoutIndicesByTreePosition.get(treePosition); if (layoutIndex !== undefined) { const layoutEntry = layoutEntries[layoutIndex]; - layoutDependenciesBefore[layoutIndex] = [...pageDependencies]; + layoutDependenciesBefore[layoutIndex] = + pageDependencies.length === 0 ? EMPTY_APP_RENDER_DEPENDENCIES : [...pageDependencies]; if (getDefaultExport(layoutEntry.layoutModule)) { const layoutDependency = createAppRenderDependency(); - layoutDependenciesByIndex.set(layoutIndex, layoutDependency); + layoutDependenciesByIndex[layoutIndex] = layoutDependency; renderDependenciesByElementId.set(layoutEntry.id, layoutDependency); pageDependencies.push(layoutDependency); } - slotDependenciesByLayoutIndex[layoutIndex] = [...pageDependencies]; + if (hasSlots) { + slotDependenciesByLayoutIndex[layoutIndex] = + pageDependencies.length === 0 ? EMPTY_APP_RENDER_DEPENDENCIES : [...pageDependencies]; + } } - const templateEntry = templateEntriesByTreePosition.get(treePosition); - if (!templateEntry || !getDefaultExport(templateEntry.templateModule)) { - continue; - } + if (hasTemplates && templateDependenciesById && templateDependenciesBeforeById) { + const templateEntry = templateEntriesByTreePosition.get(treePosition); + if (!templateEntry || !getDefaultExport(templateEntry.templateModule)) { + continue; + } - const templateDependency = createAppRenderDependency(); - templateDependenciesById.set(templateEntry.id, templateDependency); - templateDependenciesBeforeById.set(templateEntry.id, [...pageDependencies]); - pageDependencies.push(templateDependency); + const templateDependency = createAppRenderDependency(); + templateDependenciesById.set(templateEntry.id, templateDependency); + templateDependenciesBeforeById.set( + templateEntry.id, + pageDependencies.length === 0 ? EMPTY_APP_RENDER_DEPENDENCIES : [...pageDependencies], + ); + pageDependencies.push(templateDependency); + } } const routeLoadingComponent = getDefaultExport(options.route.loading); @@ -666,7 +783,7 @@ export function buildAppPageElements< continue; } const TemplateComponent = templateComponent; - const templateDependency = templateDependenciesById.get(templateEntry.id); + const templateDependency = templateDependenciesById?.get(templateEntry.id); const templateElement = templateDependency ? ( renderWithAppDependencyBarrier( @@ -681,7 +798,7 @@ export function buildAppPageElements< ); elements[templateEntry.id] = renderAfterAppDependencies( templateElement, - templateDependenciesBeforeById.get(templateEntry.id) ?? [], + templateDependenciesBeforeById?.get(templateEntry.id) ?? EMPTY_APP_RENDER_DEPENDENCIES, ); } @@ -711,17 +828,19 @@ export function buildAppPageElements< ), }; - for (const slot of Object.values(options.route.slots ?? {})) { - const slotName = slot.name; - const targetIndex = slot.layoutIndex >= 0 ? slot.layoutIndex : layoutEntries.length - 1; - if (targetIndex !== index) { - continue; + if (hasSlots) { + for (const [, slot] of slotEntries) { + const slotName = slot.name; + const targetIndex = slot.layoutIndex >= 0 ? slot.layoutIndex : layoutEntries.length - 1; + if (targetIndex !== index) { + continue; + } + layoutProps[slotName] = ; } - layoutProps[slotName] = ; } const LayoutComponent = layoutComponent; - const layoutDependency = layoutDependenciesByIndex.get(index); + const layoutDependency = layoutDependenciesByIndex[index]; const layoutElement = layoutDependency ? ( renderWithAppDependencyBarrier( @@ -740,7 +859,7 @@ export function buildAppPageElements< ); } - for (const [slotKey, slot] of Object.entries(options.route.slots ?? {})) { + for (const [slotKey, slot] of slotEntries) { const slotName = slot.name; const targetIndex = slot.layoutIndex >= 0 ? slot.layoutIndex : layoutEntries.length - 1; const treePath = layoutEntries[targetIndex]?.treePath ?? "/"; @@ -1025,7 +1144,7 @@ export function buildAppPageElements< options.matchedParams, ), }; - for (const [slotKey, slot] of Object.entries(options.route.slots ?? {})) { + for (const [slotKey, slot] of slotEntries) { const slotName = slot.name; const targetIndex = slot.layoutIndex >= 0 ? slot.layoutIndex : layoutEntries.length - 1; if (targetIndex !== layoutIndex) { @@ -1042,12 +1161,16 @@ export function buildAppPageElements< {layoutHasElement ? ( 0 + ? createAppPageParallelSlotEntries( + layoutIndex, + layoutEntries, + slotEntries, + getEffectiveSlotParams, + ) + : undefined + } > {segmentChildren} From f0db5314c7675f89d47cc400ca498c693a2739ab Mon Sep 17 00:00:00 2001 From: Nathan Nguyen <146415969+NathanDrake2406@users.noreply.github.com> Date: Sat, 20 Jun 2026 19:14:49 +1000 Subject: [PATCH 2/4] perf(server): cleanup and optimize route wiring plan lookups --- .../src/server/app-page-route-wiring.tsx | 67 ++++++++----------- 1 file changed, 27 insertions(+), 40 deletions(-) diff --git a/packages/vinext/src/server/app-page-route-wiring.tsx b/packages/vinext/src/server/app-page-route-wiring.tsx index 5dc0943a8..8be7b2dc0 100644 --- a/packages/vinext/src/server/app-page-route-wiring.tsx +++ b/packages/vinext/src/server/app-page-route-wiring.tsx @@ -231,18 +231,18 @@ type AppPageRouteWiringPlan< > = { defaultSourcePage: string; errorEntries: readonly AppPageErrorEntry[]; - errorEntriesByTreePosition: ReadonlyMap>; + errorEntriesByTreePosition: readonly (AppPageErrorEntry | undefined)[]; layoutEntries: readonly AppPageLayoutEntry[]; - layoutEntriesByTreePosition: ReadonlyMap>; + layoutEntriesByTreePosition: readonly (AppPageLayoutEntry | undefined)[]; layoutIds: readonly string[]; - layoutIndicesByTreePosition: ReadonlyMap; + layoutIndicesByTreePosition: readonly (number | undefined)[]; orderedTreePositions: readonly number[]; rootLayoutTreePath: string | null; routeSegments: readonly string[]; slotEntries: readonly AppPageSlotEntry[]; slotNameCounts: ReadonlyMap | null; templateEntries: readonly AppPageTemplateEntry[]; - templateEntriesByTreePosition: ReadonlyMap>; + templateEntriesByTreePosition: readonly (AppPageTemplateEntry | undefined)[]; }; const appPageRouteWiringPlanCache = new WeakMap< @@ -424,21 +424,21 @@ function createAppPageRouteWiringPlan< const layoutEntries = createAppPageLayoutEntries(route); const templateEntries = createAppPageTemplateEntries(route); const errorEntries = createAppPageErrorEntries(route); - const layoutEntriesByTreePosition = new Map>(); - const templateEntriesByTreePosition = new Map>(); - const errorEntriesByTreePosition = new Map>(); - const layoutIndicesByTreePosition = new Map(); + const layoutEntriesByTreePosition: (AppPageLayoutEntry | undefined)[] = []; + const templateEntriesByTreePosition: (AppPageTemplateEntry | undefined)[] = []; + const errorEntriesByTreePosition: (AppPageErrorEntry | undefined)[] = []; + const layoutIndicesByTreePosition: (number | undefined)[] = []; for (let index = 0; index < layoutEntries.length; index++) { const layoutEntry = layoutEntries[index]; - layoutEntriesByTreePosition.set(layoutEntry.treePosition, layoutEntry); - layoutIndicesByTreePosition.set(layoutEntry.treePosition, index); + layoutEntriesByTreePosition[layoutEntry.treePosition] = layoutEntry; + layoutIndicesByTreePosition[layoutEntry.treePosition] = index; } for (const templateEntry of templateEntries) { - templateEntriesByTreePosition.set(templateEntry.treePosition, templateEntry); + templateEntriesByTreePosition[templateEntry.treePosition] = templateEntry; } for (const errorEntry of errorEntries) { - errorEntriesByTreePosition.set(errorEntry.treePosition, errorEntry); + errorEntriesByTreePosition[errorEntry.treePosition] = errorEntry; } const slotEntries: AppPageSlotEntry[] = route.slots @@ -550,12 +550,6 @@ function resolveAppPageSlotBindingState( return "unmatched"; } -function resolveEmptyAppPageSlotOverride(): - | AppPageSlotOverride - | undefined { - return undefined; -} - function createAppPageSlotBindings< TModule extends AppPageModule, TErrorModule extends AppPageErrorModule, @@ -657,15 +651,12 @@ export function buildAppPageElements< const layoutIndicesByTreePosition = routePlan.layoutIndicesByTreePosition; const slotEntries = routePlan.slotEntries; const hasSlots = slotEntries.length > 0; - const hasTemplates = templateEntries.length > 0; const layoutDependenciesByIndex: AppRenderDependency[] = []; const renderDependenciesByElementId = new Map(); const layoutDependenciesBefore: Array = []; const slotDependenciesByLayoutIndex: Array = []; - const templateDependenciesById = hasTemplates ? new Map() : null; - const templateDependenciesBeforeById = hasTemplates - ? new Map() - : null; + const templateDependenciesById = new Map(); + const templateDependenciesBeforeById = new Map(); const pageDependencies: AppRenderDependency[] = []; const rootLayoutTreePath = routePlan.rootLayoutTreePath; const slotNameCounts = routePlan.slotNameCounts; @@ -688,7 +679,7 @@ export function buildAppPageElements< return undefined; } - : resolveEmptyAppPageSlotOverride; + : () => undefined; const elements: Record< string, | ReactNode @@ -729,11 +720,12 @@ export function buildAppPageElements< resolveSlotOverride?.(slotKey, slotName)?.params ?? options.matchedParams; for (const treePosition of orderedTreePositions) { - const layoutIndex = layoutIndicesByTreePosition.get(treePosition); + const layoutIndex = layoutIndicesByTreePosition[treePosition]; if (layoutIndex !== undefined) { const layoutEntry = layoutEntries[layoutIndex]; - layoutDependenciesBefore[layoutIndex] = + const deps = pageDependencies.length === 0 ? EMPTY_APP_RENDER_DEPENDENCIES : [...pageDependencies]; + layoutDependenciesBefore[layoutIndex] = deps; if (getDefaultExport(layoutEntry.layoutModule)) { const layoutDependency = createAppRenderDependency(); layoutDependenciesByIndex[layoutIndex] = layoutDependency; @@ -741,17 +733,12 @@ export function buildAppPageElements< pageDependencies.push(layoutDependency); } if (hasSlots) { - slotDependenciesByLayoutIndex[layoutIndex] = - pageDependencies.length === 0 ? EMPTY_APP_RENDER_DEPENDENCIES : [...pageDependencies]; + slotDependenciesByLayoutIndex[layoutIndex] = deps; } } - if (hasTemplates && templateDependenciesById && templateDependenciesBeforeById) { - const templateEntry = templateEntriesByTreePosition.get(treePosition); - if (!templateEntry || !getDefaultExport(templateEntry.templateModule)) { - continue; - } - + const templateEntry = templateEntriesByTreePosition[treePosition]; + if (templateEntry && getDefaultExport(templateEntry.templateModule)) { const templateDependency = createAppRenderDependency(); templateDependenciesById.set(templateEntry.id, templateDependency); templateDependenciesBeforeById.set( @@ -783,7 +770,7 @@ export function buildAppPageElements< continue; } const TemplateComponent = templateComponent; - const templateDependency = templateDependenciesById?.get(templateEntry.id); + const templateDependency = templateDependenciesById.get(templateEntry.id); const templateElement = templateDependency ? ( renderWithAppDependencyBarrier( @@ -798,7 +785,7 @@ export function buildAppPageElements< ); elements[templateEntry.id] = renderAfterAppDependencies( templateElement, - templateDependenciesBeforeById?.get(templateEntry.id) ?? EMPTY_APP_RENDER_DEPENDENCIES, + templateDependenciesBeforeById.get(templateEntry.id) ?? EMPTY_APP_RENDER_DEPENDENCIES, ); } @@ -1070,9 +1057,9 @@ export function buildAppPageElements< options.matchedParams, ); let segmentChildren: ReactNode = routeChildren; - const layoutEntry = layoutEntriesByTreePosition.get(treePosition); - const templateEntry = templateEntriesByTreePosition.get(treePosition); - const errorEntry = errorEntriesByTreePosition.get(treePosition); + const layoutEntry = layoutEntriesByTreePosition[treePosition]; + const templateEntry = templateEntriesByTreePosition[treePosition]; + const errorEntry = errorEntriesByTreePosition[treePosition]; // Next.js nesting per segment (outer to inner): Layout > Template > Error > Unauthorized > Forbidden > NotFound > children. // Building bottom-up means NotFoundBoundary must wrap the leaf subtree first, @@ -1136,7 +1123,7 @@ export function buildAppPageElements< continue; } const layoutHasElement = getDefaultExport(layoutEntry.layoutModule) !== null; - const layoutIndex = layoutIndicesByTreePosition.get(treePosition) ?? -1; + const layoutIndex = layoutIndicesByTreePosition[treePosition] ?? -1; const segmentMap: { children: string[] } & Record = { children: resolveAppPageChildSegments( routeSegments, From 664f0c6ce6821a287c6c7aaf2f8507d66ba9fe5a Mon Sep 17 00:00:00 2001 From: Nathan Nguyen <146415969+NathanDrake2406@users.noreply.github.com> Date: Sun, 21 Jun 2026 03:36:19 +1000 Subject: [PATCH 3/4] fix(server): preserve slot layout render barriers --- .../src/server/app-page-route-wiring.tsx | 15 ++-- tests/app-page-route-wiring.test.ts | 71 +++++++++++++++++++ 2 files changed, 81 insertions(+), 5 deletions(-) diff --git a/packages/vinext/src/server/app-page-route-wiring.tsx b/packages/vinext/src/server/app-page-route-wiring.tsx index 8be7b2dc0..920fbd45e 100644 --- a/packages/vinext/src/server/app-page-route-wiring.tsx +++ b/packages/vinext/src/server/app-page-route-wiring.tsx @@ -70,6 +70,12 @@ const APP_PAGE_LAYOUT_PROBE_CHILD = ; const DEFAULT_GLOBAL_ERROR_COMPONENT = DefaultGlobalError as AppPageErrorComponent; const EMPTY_APP_RENDER_DEPENDENCIES: readonly AppRenderDependency[] = []; +function snapshotAppRenderDependencies( + dependencies: readonly AppRenderDependency[], +): readonly AppRenderDependency[] { + return dependencies.length === 0 ? EMPTY_APP_RENDER_DEPENDENCIES : [...dependencies]; +} + export type AppPageModule = Record & { default?: AppPageComponent | null | undefined; }; @@ -723,9 +729,7 @@ export function buildAppPageElements< const layoutIndex = layoutIndicesByTreePosition[treePosition]; if (layoutIndex !== undefined) { const layoutEntry = layoutEntries[layoutIndex]; - const deps = - pageDependencies.length === 0 ? EMPTY_APP_RENDER_DEPENDENCIES : [...pageDependencies]; - layoutDependenciesBefore[layoutIndex] = deps; + layoutDependenciesBefore[layoutIndex] = snapshotAppRenderDependencies(pageDependencies); if (getDefaultExport(layoutEntry.layoutModule)) { const layoutDependency = createAppRenderDependency(); layoutDependenciesByIndex[layoutIndex] = layoutDependency; @@ -733,7 +737,8 @@ export function buildAppPageElements< pageDependencies.push(layoutDependency); } if (hasSlots) { - slotDependenciesByLayoutIndex[layoutIndex] = deps; + slotDependenciesByLayoutIndex[layoutIndex] = + snapshotAppRenderDependencies(pageDependencies); } } @@ -743,7 +748,7 @@ export function buildAppPageElements< templateDependenciesById.set(templateEntry.id, templateDependency); templateDependenciesBeforeById.set( templateEntry.id, - pageDependencies.length === 0 ? EMPTY_APP_RENDER_DEPENDENCIES : [...pageDependencies], + snapshotAppRenderDependencies(pageDependencies), ); pageDependencies.push(templateDependency); } diff --git a/tests/app-page-route-wiring.test.ts b/tests/app-page-route-wiring.test.ts index a504551e7..d28b4e798 100644 --- a/tests/app-page-route-wiring.test.ts +++ b/tests/app-page-route-wiring.test.ts @@ -1291,6 +1291,77 @@ describe("app page route wiring helpers", () => { expect(body).not.toContain("page:en"); }); + it("waits for the owning layout before serializing parallel slot entries", async () => { + let activeLocale = "en"; + + async function LocaleLayout(props: Record) { + await Promise.resolve(); + activeLocale = "de"; + return createElement( + "div", + { "data-layout": "locale" }, + readChildren(props.sidebar), + readChildren(props.children), + ); + } + + function LocaleSlotPage() { + return createElement("aside", null, `slot:${activeLocale}`); + } + + function LocalePage() { + return createElement("main", null, "page"); + } + + const elements = buildAppPageElements({ + element: createElement(LocalePage), + makeThenableParams(params) { + return Promise.resolve(params); + }, + matchedParams: {}, + resolvedMetadata: null, + resolvedViewport: {}, + route: { + error: null, + errors: [null], + layoutTreePositions: [0], + layouts: [{ default: LocaleLayout }], + loading: null, + notFound: null, + notFounds: [null], + routeSegments: ["locale"], + slots: { + sidebar: { + default: null, + error: null, + layout: null, + layoutIndex: 0, + loading: null, + name: "sidebar", + page: { default: LocaleSlotPage }, + routeSegments: [], + }, + }, + templateTreePositions: [], + templates: [], + }, + routePath: "/locale", + rootNotFoundModule: null, + }); + + const body = await renderHtml( + createElement( + Fragment, + null, + readChildren(elements["layout:/"]), + readChildren(elements["slot:sidebar:/"]), + ), + ); + + expect(body).toContain("slot:de"); + expect(body).not.toContain("slot:en"); + }); + it("releases skipped layout dependencies before serializing retained child entries", async () => { let activeLocale = "en"; From 21f963a250bc435bafdba2d347750ddc999d12b3 Mon Sep 17 00:00:00 2001 From: Nathan Nguyen <146415969+NathanDrake2406@users.noreply.github.com> Date: Sun, 21 Jun 2026 03:46:55 +1000 Subject: [PATCH 4/4] perf(server): group route slots by layout --- .../src/server/app-page-route-wiring.tsx | 58 ++++++++++--------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/packages/vinext/src/server/app-page-route-wiring.tsx b/packages/vinext/src/server/app-page-route-wiring.tsx index 920fbd45e..1fafa6196 100644 --- a/packages/vinext/src/server/app-page-route-wiring.tsx +++ b/packages/vinext/src/server/app-page-route-wiring.tsx @@ -69,6 +69,7 @@ type AppPageErrorComponent = ComponentType<{ error: unknown; reset: () => void } const APP_PAGE_LAYOUT_PROBE_CHILD = ; const DEFAULT_GLOBAL_ERROR_COMPONENT = DefaultGlobalError as AppPageErrorComponent; const EMPTY_APP_RENDER_DEPENDENCIES: readonly AppRenderDependency[] = []; +const EMPTY_APP_PAGE_SLOT_ENTRIES: readonly AppPageSlotEntry[] = []; function snapshotAppRenderDependencies( dependencies: readonly AppRenderDependency[], @@ -246,6 +247,10 @@ type AppPageRouteWiringPlan< rootLayoutTreePath: string | null; routeSegments: readonly string[]; slotEntries: readonly AppPageSlotEntry[]; + slotEntriesByLayoutIndex: readonly ( + | readonly AppPageSlotEntry[] + | undefined + )[]; slotNameCounts: ReadonlyMap | null; templateEntries: readonly AppPageTemplateEntry[]; templateEntriesByTreePosition: readonly (AppPageTemplateEntry | undefined)[]; @@ -450,11 +455,17 @@ function createAppPageRouteWiringPlan< const slotEntries: AppPageSlotEntry[] = route.slots ? Object.entries(route.slots) : []; + const slotEntriesByLayoutIndex: AppPageSlotEntry[][] = []; let slotNameCounts: Map | null = null; if (slotEntries.length > 0) { slotNameCounts = new Map(); - for (const [, slot] of slotEntries) { + for (const slotEntry of slotEntries) { + const [, slot] = slotEntry; slotNameCounts.set(slot.name, (slotNameCounts.get(slot.name) ?? 0) + 1); + const targetIndex = slot.layoutIndex >= 0 ? slot.layoutIndex : layoutEntries.length - 1; + if (targetIndex >= 0) { + (slotEntriesByLayoutIndex[targetIndex] ??= []).push(slotEntry); + } } } @@ -476,6 +487,7 @@ function createAppPageRouteWiringPlan< rootLayoutTreePath: layoutEntries[0]?.treePath ?? null, routeSegments, slotEntries, + slotEntriesByLayoutIndex, slotNameCounts, templateEntries, templateEntriesByTreePosition, @@ -505,23 +517,19 @@ function createAppPageParallelSlotEntries< TModule extends AppPageModule, TErrorModule extends AppPageErrorModule, >( - layoutIndex: number, - layoutEntries: readonly AppPageLayoutEntry[], + layoutTreePath: string, slotEntries: readonly AppPageSlotEntry[], getEffectiveSlotParams: (slotKey: string, slotName: string) => AppPageParams, ): Readonly> | undefined { + if (slotEntries.length === 0) { + return undefined; + } + const parallelSlots: Record = {}; for (const [slotKey, slot] of slotEntries) { const slotName = slot.name; - const targetIndex = slot.layoutIndex >= 0 ? slot.layoutIndex : layoutEntries.length - 1; - if (targetIndex !== layoutIndex) { - continue; - } - - const layoutEntry = layoutEntries[targetIndex]; - const treePath = layoutEntry?.treePath ?? "/"; - const slotId = resolveAppPageSlotId(slot, treePath); + const slotId = resolveAppPageSlotId(slot, layoutTreePath); const slotParams = getEffectiveSlotParams(slotKey, slotName); const slotSegments = slot.routeSegments ? resolveAppPageChildSegments(slot.routeSegments, 0, slotParams) @@ -533,7 +541,7 @@ function createAppPageParallelSlotEntries< ); } - return Object.keys(parallelSlots).length > 0 ? parallelSlots : undefined; + return parallelSlots; } function resolveAppPageSlotId(slot: AppPageRouteWiringSlot, treePath: string): string { @@ -656,6 +664,7 @@ export function buildAppPageElements< const errorEntriesByTreePosition = routePlan.errorEntriesByTreePosition; const layoutIndicesByTreePosition = routePlan.layoutIndicesByTreePosition; const slotEntries = routePlan.slotEntries; + const slotEntriesByLayoutIndex = routePlan.slotEntriesByLayoutIndex; const hasSlots = slotEntries.length > 0; const layoutDependenciesByIndex: AppRenderDependency[] = []; const renderDependenciesByElementId = new Map(); @@ -723,7 +732,7 @@ export function buildAppPageElements< elements[APP_STATIC_SIBLINGS_KEY] = options.route.staticSiblings; } const getEffectiveSlotParams = (slotKey: string, slotName: string): AppPageParams => - resolveSlotOverride?.(slotKey, slotName)?.params ?? options.matchedParams; + resolveSlotOverride(slotKey, slotName)?.params ?? options.matchedParams; for (const treePosition of orderedTreePositions) { const layoutIndex = layoutIndicesByTreePosition[treePosition]; @@ -821,12 +830,8 @@ export function buildAppPageElements< }; if (hasSlots) { - for (const [, slot] of slotEntries) { + for (const [, slot] of slotEntriesByLayoutIndex[index] ?? []) { const slotName = slot.name; - const targetIndex = slot.layoutIndex >= 0 ? slot.layoutIndex : layoutEntries.length - 1; - if (targetIndex !== index) { - continue; - } layoutProps[slotName] = ; } } @@ -1129,6 +1134,10 @@ export function buildAppPageElements< } const layoutHasElement = getDefaultExport(layoutEntry.layoutModule) !== null; const layoutIndex = layoutIndicesByTreePosition[treePosition] ?? -1; + const layoutSlotEntries = + layoutIndex >= 0 + ? (slotEntriesByLayoutIndex[layoutIndex] ?? EMPTY_APP_PAGE_SLOT_ENTRIES) + : EMPTY_APP_PAGE_SLOT_ENTRIES; const segmentMap: { children: string[] } & Record = { children: resolveAppPageChildSegments( routeSegments, @@ -1136,12 +1145,8 @@ export function buildAppPageElements< options.matchedParams, ), }; - for (const [slotKey, slot] of slotEntries) { + for (const [slotKey, slot] of layoutSlotEntries) { const slotName = slot.name; - const targetIndex = slot.layoutIndex >= 0 ? slot.layoutIndex : layoutEntries.length - 1; - if (targetIndex !== layoutIndex) { - continue; - } const slotParams = getEffectiveSlotParams(slotKey, slotName); segmentMap[slotName] = slot.routeSegments ? resolveAppPageChildSegments(slot.routeSegments, 0, slotParams) @@ -1154,11 +1159,10 @@ export function buildAppPageElements< 0 + layoutSlotEntries.length > 0 ? createAppPageParallelSlotEntries( - layoutIndex, - layoutEntries, - slotEntries, + layoutEntry.treePath, + layoutSlotEntries, getEffectiveSlotParams, ) : undefined