mirror of
https://github.com/cloudflare/vinext.git
synced 2026-05-09 08:25:34 +02:00
[GH-ISSUE #982] perf: eliminate redundant layout/page probing by merging into single render pass #211
Labels
No labels
enhancement
enhancement
good first issue
help wanted
nextjs-tracking
nextjs-tracking
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
starred/vinext#211
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Originally created by @Divkix on GitHub (Apr 30, 2026).
Original GitHub issue: https://github.com/cloudflare/vinext/issues/982
Problem
Every App Router request renders each layout component twice — once for probing, once for the actual render:
app-page-execution.ts:164-233probeAppPageLayouts()renders layouts from leaf-to-root withchildren: nullapp-page-execution.ts:250-268probeAppPageComponent()renders the page separatelyapp-page-render.tsFor a route with 5 nested layouts, that's 5 extra React render + commit cycles per request. The probes are sequential (leaf-to-root), cannot be parallelized, and happen on every single request — even though a layout's classification (static vs dynamic) rarely changes between requests.
What probing does
The probe serves two purposes:
Classification (
app-page-execution.ts:194-221): Run the layout in an isolated dynamic scope to detect whether it uses dynamic APIs (headers(),cookies(), etc.). Result:"s"(static) or"d"(dynamic) stored inlayoutFlags. Used downstream for navigation optimization (static layouts can be skipped on client-side navigation).Error detection (
app-page-execution.ts:235-248): Catch redirects and notFounds from layout/page rendering before the full render starts. Allows early-exit without building the full React tree.Approach: Single-pass rendering
Step 1: Eliminate the error-detection probe
The probe catches
redirect()andnotFound()from layouts/pages to early-exit before the full tree renders. Instead, catch these during the actual render pass:redirect()andnotFound()throw special objects that error boundaries can catch.probeLayoutForErrorslogic fromapp-page-execution.ts:235-248into the render error boundaries themselves — when a boundary catches a redirect/notFound, it callsonLayoutErrordirectly and aborts the tree.Step 2: Inline classification into the render pass
The probe runs each layout with
children: nullin an isolated scope to detect dynamic API usage. Instead:runWithIsolatedDynamicScope(same function used during probing atapp-page-execution.ts:198).headers(),cookies(), etc.) were accessed during that layout's render."s"/"d"flag immediately — no separate pass needed.app-page-execution.ts:174-176), skip the ALS wrap entirely — use the pre-computed flag.Step 3: Defer
layoutFlagsembeddingThis is the key architectural challenge. Currently:
buildOutgoingAppPayload(element, layoutFlags)atapp-page-render.ts:154embeds__layoutFlagsin the RSC wire payload before the tree starts streaming.Solution options:
Option A — Post-render payload patching (recommended):
__layoutFlagsmetadataapp-elements.ts:224),readAppElementsMetadata()already extractslayoutFlagsfrom the wire payload — update it to accept a post-render metadata chunkOption B — Deferred Promise slots:
"s"or"d"as it completesStep 4: Remove
probeAppPageComponententirelyThe page component probe (
app-page-execution.ts:250-268) renders the page to catch errors. Same fix as Step 1: catch errors during the real render via the page's error boundary.Step 5: Clean up dead code
After the above is working:
probeAppPageLayoutsentirelyprobeAppPageComponententirelyprobeAppPageBeforeRenderfromapp-page-probe.tsapp-page-execution.tsto only export render utilities (no probe utilities)Files to change: 5-7
app-page-execution.tsapp-rsc-entry.ts:1822-1930app-page-probe.ts:30-84probeAppPageBeforeRender; if anything survives, inline it intoapp-page-render.tsapp-page-render.ts:149-157layoutFlagsembedding to post-render (Option A above); or accept flags produced during renderapp-elements.ts:220-240readAppElementsMetadatato handle post-render metadata chunkapp-ssr-entry.ts:170-210app-page-cache.tsKey behavior to preserve
layoutFlags["s"]tells the client "this layout hasn't changed, skip re-rendering it". This must still work.headers(),cookies(),draftMode(),connection()must still be detected per-layout for correct classification.Difficulty: Hard
Touches the core render lifecycle and the RSC wire format metadata ordering. The
layoutFlagsdeferral (Step 3) is the trickiest part.Expected improvement
20-40% faster SSR for routes with 5+ nested layouts. Every layout-heavy page gets this benefit. Even single-layout routes benefit from skipping the page component probe.
Risk: Medium-High
layoutFlagsparsing must handle reordered metadata. If the client expects flags in the first chunk, this breaks without client-side changes.@Divkix commented on GitHub (Apr 30, 2026):
Closing for now. This is a high-complexity architectural change affecting the core render lifecycle and RSC wire format metadata ordering. The complexity/risk outweighs the 20-40% SSR performance gain for routes with 5+ nested layouts at this stage of the project.
We'll revisit this optimization before v1.0 when we have more test coverage and stability in the App Router render pipeline.
cc @divkix