Skip to content

feat(skip): enable proven static layout transport skips#1437

Open
NathanDrake2406 wants to merge 23 commits into
cloudflare:mainfrom
NathanDrake2406:nathan/726-skip-04-05-static-layout-skip-transport
Open

feat(skip): enable proven static layout transport skips#1437
NathanDrake2406 wants to merge 23 commits into
cloudflare:mainfrom
NathanDrake2406:nathan/726-skip-04-05-static-layout-skip-transport

Conversation

@NathanDrake2406
Copy link
Copy Markdown
Contributor

@NathanDrake2406 NathanDrake2406 commented May 22, 2026

Overview

Implements #726-SKIP-04/05 from #726.

Bonk: please read issue #726 for the architectural big picture before reviewing this slice.

Item Summary
Goal Enable RSC skip transport for layouts only when the server can prove the client already has the exact public static layout artifact.
Core change Client navigation sends a bounded reuse manifest, dispatch verifies it against current render proof, and RSC encoding omits only verified static layout records.
Safety boundary The manifest is a hint. Server verification, layout classification, cache proof, compatibility, payload hash, invalidation, structural per-layout param scope, request API observation, finite layout revalidate, and per-layout data observations decide whether bytes may be skipped.
Primary review path client-reuse-manifest, skip-cache-proof, app-page-render, app-page-dispatch, app-layout-param-observation, app-page-probe, app-page-route-wiring, app-page-params, app-browser-client-reuse-manifest, generated app-rsc-entry wiring.
Expected impact Proven static shared layouts can be retained across soft navigations. Param-scoped, data-dependent, finite-revalidate, dynamic, malformed, oversized, cache-unsafe, or unproven cases render and send normally.

Why

Skip transport is only correct when the retained client layout is indistinguishable from the target server layout for this request. That means the client manifest cannot authorize omission by itself, partial RSC responses cannot become shared cache artifacts, and a layout whose output can depend on params, data invalidation, or finite revalidation cannot be reused without stronger proof. This PR keeps those responsibilities at the server render boundary and treats uncertain layouts conservatively.

Area Principle / invariant What this PR changes
Client manifest Hints are untrusted input, and browser-produced hints must stay within the server verification budget. The browser sends a bounded manifest only for normal soft navigation, caps produced entries to the skip planner budget, and parsing still returns render/send by default.
Server proof Omission requires current target proof. Dispatch threads the parsed manifest into render lifecycle, where current layout flags, compatibility, variant, payload hash, invalidation, and static layout proof are checked.
Layout params Static classification is not enough if layout output can depend on params. The tracker records each layout's structural segment-param scope from route segment names, including optional catch-all names when the runtime value is empty. Any non-empty scope blocks skip for that layout.
Layout data and request APIs Static classification is not enough if layout output can depend on request APIs or cached/dynamic data. Each layout probe observes request APIs, cache life, fetch tags, cacheable fetches, dynamic fetches, and unstable_cache() usage in an isolated child request scope before skip verification. Any observed dependency blocks skip for that layout without leaking into sibling or parent probe observations.
Layout revalidate A finite ISR policy is not a permanent retained artifact proof. Per-layout observation records finite positive revalidate segment config, and skip rejects those layouts with SKIP_LAYOUT_REVALIDATE_PRESENT.
Probe shape and depth Dependencies can live below the shallow layout function body, and some layouts render chrome only when children is present. Probes must not mutate single-use children. Generated layout probes delegate into a typed helper that passes a terminal non-null child, evaluates returned server-component children including memo()/forwardRef()/React.lazy wrappers, and enforces bounded depth/node limits. Array branches count against those limits. Non-array iterables are rejected without iteration so skip falls back instead of consuming stateful children.
Param observation Direct reads still provide traceability and future proofing. Layout params observation records sync reads, await params, destructuring/enumeration, and in checks, while skip authorization now requires both complete observation and empty structural param scope.
Caching Skipped payloads are per-client transport artifacts. Enabled skip bypasses RSC capture and ISR writes, returns Cache-Control: no-store, must-revalidate, and avoids visited-response replay.

What changed

Scenario / surface Before After
Browser soft navigation No client reuse manifest was attached, and later producer limits could exceed the server verification budget. navigateRsc() attaches X-Vinext-Client-Reuse-Manifest for retained public static layouts, capped to the skip planner entry budget before byte-size trimming.
Server request path Parsed manifests could be dropped before render. The parsed manifest flows through handler, generated entry, dispatch, and render lifecycle.
Verified static layout RSC payload Full layout record was always sent. Proven layout records may be omitted from RSC payloads only. HTML still sends the full tree.
Shared caches A skipped RSC stream could be captured under the normal cache key. Skip-enabled responses are no-store and are not captured for ISR/page cache writes.
Dynamic segment layouts A layout classified static could pass params to children and be incorrectly retained across sibling values. Empty optional catch-all targets could look param-free because the runtime params object was {}. Resolved structural per-layout param scope gates skip. Layouts under dynamic, catch-all, or optional catch-all segment names render and send, even when the current value is empty or the shallow probe saw no param read.
Layout request/data dependencies Skip proof manufactured request-api-clean and cache-clean observations for static layouts, and client-facing layout flags could still advertise those layouts as static. Skip proof now requires complete per-layout observation, rejects layouts whose probe observed request APIs, cache life, fetch tags, cacheable fetches, dynamic fetches, or unstable_cache() usage, and publishes those unsafe observations as dynamic layout metadata.
Finite layout revalidate Static layout skip could still be considered for finite revalidate segment config. Finite positive layout revalidate is tracked per layout and blocks skip.
Layout metadata Runtime and build-time-static probes could publish "s" layout flags even after per-layout observation proved skip would be rejected. Layout flags now consult the same unsafe-observation predicate used by skip authorization, so request APIs, params, finite revalidate, cacheLife, cache tags, cacheable fetches, dynamic fetches, unstable_cache() usage, and incomplete probes publish "d".
Layout probe child shape Layouts that branch on children could hide chrome dependencies because probes rendered with children={null}. The probe renders layouts with an inert non-null React child, so children-truthy chrome branches are observed without descending into page content.
Wrapped layout children memo(), forwardRef(), and React.lazy returned below a layout could hide data dependencies from the probe, while arbitrary iterable children could be single-use. The bounded subtree probe renders through wrappers before skip verification, arrays consume the same depth and node budgets as element branches, and non-array iterables are treated as unprobeable without advancing them.
Browser visited cache Partial payloads could be restorable without matching retained layouts. Skip-pruned payload metadata is non-restorable, and approved commits preserve explicitly skipped retained layouts.
Maintainer review path
File What to review
packages/vinext/src/server/client-reuse-manifest.ts Parser limits, rejection codes, and the untrusted parse-time disposition contract.
packages/vinext/src/server/skip-cache-proof.ts Static-layout skip planning, exact compatibility requirements, mixed rejection traces, and over-budget fallback.
packages/vinext/src/server/app-page-render.ts Runtime upgrade from manifest hint to skip disposition, layout-only omission boundary, cache bypass, and param/request-api/data/revalidate observation rejection.
packages/vinext/src/server/app-page-dispatch.ts Request dispatch wiring, layout observation tracker ownership, and propagation into intercept and render paths.
packages/vinext/src/server/app-layout-param-observation.ts Per-layout completeness, observed param keys, structural param scope, finite revalidate, and isolated request API, cache life, cache tag, cacheable fetch, and dynamic fetch observations.
packages/vinext/src/server/app-page-probe.ts Bounded returned-subtree probing for layout server-component children, including memo()/forwardRef()/React.lazy wrappers, array traversal limits, and non-array iterable fail-closed behavior.
packages/vinext/src/server/app-page-route-wiring.tsx Route-specific layout probe helper, structural scope recording, finite revalidate recording, and generated-entry delegation boundary.
packages/vinext/src/server/app-page-params.ts Difference between runtime params values and structural segment-param scope names.
packages/vinext/src/entries/app-rsc-entry.ts Generated App Router wiring for parsed manifests, build-page callbacks, layout probes, and helper delegation.
packages/vinext/src/shims/fetch-cache.ts Cacheable fetch observation alongside existing dynamic fetch and tag collection.
packages/vinext/src/shims/thenable-params.ts Sync and async params observation, including await params, destructuring, enumeration, and promise continuations.
packages/vinext/src/server/app-browser-client-reuse-manifest.ts and browser entry tests Bounded manifest production from visible AppElements state, producer-side verification-budget capping, and navigation-only attachment.
Validation
Command Coverage
vp check Formatting, lint, and typecheck. Passed: all 1406 files formatted, no warnings, lint errors, or type errors in 649 files.
vp test run tests/app-page-probe.test.ts tests/app-layout-param-observation.test.ts tests/app-page-dispatch.test.ts Layout subtree probing, including memo()/forwardRef()/React.lazy wrappers and array traversal limits, isolated per-layout dependency observation, plus dispatch skip rejection for wrapped cache-tagged layout children. Passed: 3 files, 43 tests.
`vp test run tests/app-page-probe.test.ts -t "single-use iterables subtree .*limits"`
vp test run tests/app-page-probe.test.ts tests/app-page-dispatch.test.ts tests/app-page-render.test.ts tests/app-layout-param-observation.test.ts Probe, dispatch, render lifecycle, and tracker coverage after non-array iterables became fail-closed and after unsafe layout observations were bridged into client-facing layout flags. Passed: 4 files, 80 tests.
`vp test run tests/app-page-dispatch.test.ts -t "request-api layouts dynamic probe observes request APIs"`
vp test run tests/app-page-dispatch.test.ts tests/app-layout-param-observation.test.ts tests/app-page-render.test.ts tests/skip-cache-proof.test.ts Dispatch, tracker, render lifecycle, and skip planner coverage after request API observations were added to the layout skip gate. Passed: 4 files, 82 tests.
vp test run tests/app-layout-param-observation.test.ts tests/app-page-dispatch.test.ts -t "unstable_cache" Focused regressions that layout probes record unstable_cache() dependencies on cache miss and hit, and that verified static-layout skip falls back to render/send for retained layouts backed by unstable_cache() data. Passed: 2 tests.
vp test run tests/app-layout-param-observation.test.ts tests/app-page-dispatch.test.ts tests/app-page-render.test.ts tests/shims.test.ts tests/unified-request-context.test.ts tests/skip-cache-proof.test.ts Cache shim, unified request context, layout observation, dispatch, render lifecycle, and skip planner coverage after adding the unstable_cache() observation channel. Passed: 6 files, 1094 tests.
vp test run tests/app-browser-client-reuse-manifest.test.ts tests/client-reuse-manifest.test.ts tests/skip-cache-proof.test.ts Browser producer cap, manifest parser limits, and skip planner budget behaviour. Passed: 3 files, 42 tests.
vp test run tests/app-page-render.test.ts tests/app-router.test.ts Render lifecycle plus App Router integration after the wrapped-subtree probe change. Passed: 2 files, 364 tests.
`vp test run tests/app-page-dispatch.test.ts -t "finite revalidate cache-tagged data"`
`vp test run tests/app-page-route-wiring.test.ts -t "children are present returned layout children"`
vp test run tests/app-page-route-wiring.test.ts tests/app-page-execution.test.ts tests/app-page-probe.test.ts tests/app-page-dispatch.test.ts tests/app-page-render.test.ts tests/app-layout-param-observation.test.ts Route wiring, lifecycle probing, subtree probing, dispatch, render lifecycle, and tracker coverage after child-shape-equivalent probing and the build-time special-error flag regression fix. Passed: 6 files, 136 tests.
vp test run tests/app-router.test.ts tests/entry-templates.test.ts tests/app-page-route-wiring.test.ts Generated entry wiring, route-wiring helper coverage, and App Router integration after probe child-shape changes. Passed: 3 files, 380 tests.
vp test run tests/app-page-dispatch.test.ts tests/app-page-route-wiring.test.ts tests/app-page-probe.test.ts tests/app-page-render.test.ts tests/app-router.test.ts Dispatch, route wiring, layout subtree probing, render lifecycle, and App Router integration. Passed: 5 files, 428 tests.
vp test run tests/app-page-probe.test.ts tests/app-page-dispatch.test.ts tests/app-page-render.test.ts tests/app-browser-client-reuse-manifest.test.ts tests/client-reuse-manifest.test.ts tests/skip-cache-proof.test.ts Skip planner, manifest parser/producer, render lifecycle, request dispatch, subtree probing, and cache/data dependency rejection. Passed: 6 files, 109 tests.
vp test run tests/app-router.test.ts tests/entry-templates.test.ts tests/app-page-request.test.ts tests/app-elements.test.ts tests/app-page-route-wiring.test.ts tests/thenable-params.test.ts tests/app-page-params.test.ts Generated entry wiring, App Router integration, request helper regression, payload encoding, route wiring, thenable params, and structural param scope. Passed: 7 files, 494 tests.
vp run vinext#build Package build and generated type output. Passed with the existing unresolved virtual/private import warnings for private-next-instrumentation-client, virtual:vinext-rsc-entry, and virtual:vite-rsc/client-references.
PLAYWRIGHT_PROJECT=app-router pnpm exec playwright test tests/e2e/app-router/layout-persistence.spec.ts --project=app-router -g "dynamic segment layout counter survives param navigation while content updates" Regression for sibling dynamic values preserving stale layout params. Passed: 1 test.
vp test run tests/app-page-execution.test.ts -t "build-time classified layout throws" Local reproduction for the failed CI unit test. Passed: 1 test.
git diff --check Whitespace sanity check. Passed.
Commit hook Checked-file formatting, lint/typecheck, and knip. Passed.

Previously on this PR, the navigation manifest and visited-cache e2e slice also passed:

CI=true PLAYWRIGHT_PROJECT=app-router npx playwright test tests/e2e/app-router/build-id-navigation.spec.ts tests/e2e/app-router/nextjs-compat/actions-revalidate.spec.ts tests/e2e/app-router/nextjs-compat/search-params-key.spec.ts tests/e2e/app-router/advanced.spec.ts -g "client reuse manifest|visited RSC|discarded server action|router.push|refresh on direct target"
Risk / compatibility
Surface Risk handling
Public API No public API change. The new header is an internal vinext client/server transport hint.
RSC transport Omission is restricted to layout element keys on RSC responses. HTML, pages, templates, routes, slots, dynamic layouts, structurally param-scoped layouts, finite-revalidate layouts, and data-dependent layouts still send full payloads.
Cache correctness Skip-enabled responses are no-store, bypass RSC capture, and cannot be written to ISR under a normal RSC key.
Param-dependent layouts Conservative by design. Any non-empty structural layout param scope blocks skip for that layout, including optional catch-all segments whose runtime value is empty and cases where params are only passed to child components.
Request/data-dependent layouts Conservative by design. Request APIs, cache life, fetch tags, cacheable fetches, dynamic fetches, and unstable_cache() usage observed during isolated layout probing block skip rather than relying on synthetic clean proof. Wrapped memo()/forwardRef()/React.lazy children are probed too. unstable_cache() trace metadata uses hashed keys and tag metadata, not raw keys or values.
Finite revalidate Conservative by design. Finite positive layout revalidate blocks skip because the retained layout has a revalidation policy, not a permanent artifact proof.
Manifest cardinality The client manifest remains bounded, browser-produced manifests stay within the server verification budget, and skipped payloads are not made shared cache variants.
Existing apps Unproven cases fall back to current render/send behavior. The main risk is missed optimization, not stale UI.
Non-goals

References

Reference Why it matters
#726 Architectural context for the skip transport, compatibility, and cache-proof model.
#726-SKIP-04/05 Task slice implemented by this PR.
tests/app-page-dispatch.test.ts Lowest honest dispatch-boundary coverage for verified skip, dynamic fallback, cache bypass, param-scoped layout rejection, request-API rejection, unstable_cache() rejection, optional catch-all empty-target rejection, data-dependent layout rejection including wrapped children and React.lazy children, finite-revalidate rejection, and hostile param-dependent layouts.
tests/app-browser-client-reuse-manifest.test.ts Focused coverage for browser manifest production, including server verification-budget capping.
tests/app-page-route-wiring.test.ts Focused coverage for layout probe helper delegation, returned subtree probing, and revalidate tracking.
tests/app-layout-param-observation.test.ts Focused coverage for probe-local request API, fetch/cacheLife, and unstable_cache() dependency observation isolation across child, parent, and sibling layouts.
tests/app-page-probe.test.ts Focused coverage that layout probing invokes returned server-component children, including memo()/forwardRef()/React.lazy wrappers, enforces array depth/node limits, and does not consume single-use iterable children.
tests/app-page-params.test.ts Focused coverage for runtime params value scoping versus structural layout param scope.
tests/thenable-params.test.ts Focused coverage for sync, async, and destructured params observation.

Closes #726

Authorize skip transport only after a parsed client reuse manifest entry cross-checks against a public static layout cache proof. Add a skip transport planner that preserves rejection traces, falls back to render/send for absent, rejected, oversized, or over-budget manifests, and uses canonical parsed entry ids rather than verifier-provided ids.

Wire enabled static-layout skip disposition into the RSC payload encoder so only proven layout records are omitted. HTML responses and non-layout entries still render and send the full tree.
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 22, 2026

Open in StackBlitz

npm i https://pkg.pr.new/vinext@1437

commit: e539ec0

@NathanDrake2406 NathanDrake2406 marked this pull request as draft May 22, 2026 10:58
Canonical RSC requests could parse X-Vinext-Client-Reuse-Manifest but still send full AppElements payloads. The parsed manifest stopped at the App RSC handler, so dispatch and render never had the proof input needed to plan static layout omission.

Thread the parsed manifest through generated App RSC dispatch into dispatchAppPage and renderAppPageLifecycle, then build the skip disposition from current static layout classifications and layout-scoped proof metadata before encoding the RSC payload. HTML renders still ignore skip dispositions.

Tests cover header parsing into dispatch, generated entry wiring, static-only dispatch omission, dynamic fallback, and HTML full-payload behaviour.
Verified skip transport can omit layout records for a specific client. Caching that partial RSC stream under the normal route key is unsafe because later clients may not have the omitted layout locally.

Bypass RSC capture and ISR cache scheduling when skip transport is enabled, force the client response to no-store, and emit a debug trace for the skipped cache write. Tests cover the render lifecycle cache boundary and the manifest-driven dispatch path.
@james-elicx
Copy link
Copy Markdown
Member

/bigbonk review

Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: feat(skip): enable proven static layout transport skips

The architecture is sound. The planner stays untrusted-at-parse / verified-at-runtime, the cache bypass is correctly wired end-to-end, and the test coverage is thorough. The inline comments below are refinements, not blockers.

What's good

  • Trust boundary is clean. Parser disposition stays renderAndSend; only the runtime planner can upgrade to SKIP_STATIC_LAYOUT_VERIFIED after server-side verification. The hardening from #726 is preserved.
  • Cache bypass is correct. shouldCaptureRscForCacheMetadata is gated off, capturedRscDataPromise is null, scheduleAppPageRscCacheWrite returns false, finalizeAppPageRscCacheResponse returns the response unchanged, and no-store, must-revalidate is set. No shared cache poisoning path.
  • Layout-only omission guard is correct. createSkippedLayoutIds re-parses each entry ID through parseAppElementsWireElementKey and only admits kind: "layout", so a malicious manifest that smuggles a page/template/route entry ID into skippedEntryIds cannot omit non-layout content.
  • Budget enforcement — the maxEntriesToVerify check rejects over-budget manifests before the verifier loop runs. The test for this is explicit.
  • Entry ID hardeningskippedEntryIds is populated from entry.id (the canonical parsed entry), not from verifier-returned IDs. The "uses the verified manifest entry id instead of trusting verifier-provided skipped ids" test proves this.
  • HTML path is untouched — skip disposition is only passed to encodeOutgoingPayload for RSC requests, and the dispatch test proves HTML responses still contain the full tree.

Nits and suggestions below.

Comment thread packages/vinext/src/server/app-page-render.ts
Comment thread packages/vinext/src/server/app-page-render.ts
Comment thread packages/vinext/src/server/skip-cache-proof.ts Outdated
Comment thread packages/vinext/src/server/skip-cache-proof.ts
Comment thread packages/vinext/src/server/app-elements-wire.ts Outdated
Comment thread packages/vinext/src/server/app-page-render.ts
Comment thread tests/app-page-dispatch.test.ts
Comment thread packages/vinext/src/server/app-page-render.ts Outdated
@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented May 22, 2026

Review submitted. Here's a summary of what I found:

Overall: The PR is solid. The trust boundary design from #726 is correctly preserved — parse-time disposition stays renderAndSend, and only the runtime planner can upgrade to SKIP_STATIC_LAYOUT_VERIFIED after server-side verification. The cache bypass is correctly wired end-to-end (capture disabled, ISR write skipped, no-store set, no shared cache poisoning path).

8 inline comments posted, all non-blocking refinements:

  1. Dead verifyEntry callback in the non-record/no-compatibility fallback path — the maxEntriesToVerify: 0 budget means the callback can never execute. Suggested simplification.
  2. Synthetic render observation for static layout verification assumes probe-time classification is authoritative — suggested documenting this assumption.
  3. Duplicate type aliases (StaticLayoutClientReusePayloadHashInput and StaticLayoutClientReuseArtifactCompatibilityInput) have identical fields — could be unified.
  4. Budget check conflates invalid vs exceeded — both produce SKIP_ENTRY_COUNT_EXCEEDED with misleading maxEntryCount: 0 for invalid budgets.
  5. Empty Set allocation on hot pathcreateSkippedLayoutIds allocates a new empty Set on every response when skip is disabled. Suggested module-level constant.
  6. Missing cacheState is intentional but non-obvious — the skip-bypass policy omits cacheState which has a downstream effect on preserveClientResponseHeaders. Suggested documenting intent.
  7. Dispatch test missing waitUntil assertion — render-level test covers it but dispatch boundary test doesn't verify no ISR write was scheduled.
  8. Budget constant placementSTATIC_LAYOUT_SKIP_VERIFICATION_ENTRY_BUDGET is policy, could live in skip-cache-proof.ts.

github run

Browser navigation could only benefit from skip transport when an external caller manually supplied X-Vinext-Client-Reuse-Manifest. That left normal App Router soft navigations rendering full layout payloads even when the current visible state retained a proven public static layout.

The missing boundary was the browser request path. Build a bounded manifest from visible AppElements metadata, reuse the same static-layout compatibility and payload-hash proof contract as server verification, and attach it through createRscRequestHeaders only for navigation fetches.

Also folds in Bonk review refinements around skip planner policy ownership, invalid verification budgets, synthetic static-layout observations, no-store cache semantics, and dispatch-level waitUntil coverage.
@NathanDrake2406 NathanDrake2406 marked this pull request as ready for review May 22, 2026 12:31
@james-elicx james-elicx marked this pull request as draft May 22, 2026 13:17
@chatgpt-codex-connector
Copy link
Copy Markdown

To use Codex here, create an environment for this repo.

@NathanDrake2406
Copy link
Copy Markdown
Contributor Author

@codex review

@NathanDrake2406 NathanDrake2406 marked this pull request as ready for review May 22, 2026 14:04
Copilot AI review requested due to automatic review settings May 22, 2026 14:04
@NathanDrake2406 NathanDrake2406 marked this pull request as draft May 22, 2026 14:04
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enables verified static-layout skip transport for App Router RSC navigations by introducing an untrusted client reuse manifest header, parsing it at the canonical RSC boundary, and upgrading to an enabled skip disposition only after server-side proof cross-checks at the render lifecycle boundary. It also ensures skipped (client-specific) RSC payloads bypass shared caching and are returned with Cache-Control: no-store, must-revalidate.

Changes:

  • Add a browser-side producer for a bounded X-Vinext-Client-Reuse-Manifest derived from visible retained static layout entries, and attach it to real RSC navigation requests.
  • Add server-side skip transport planning/proofing to upgrade from parsed (disabled) disposition to SKIP_STATIC_LAYOUT_VERIFIED, and omit only proven layout records from RSC payload encoding.
  • Add extensive wiring + boundary tests (planner behavior, request normalization/dispatch plumbing, cache bypass/no-store behavior, and e2e header emission).

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/skip-cache-proof.test.ts Expands unit coverage to assert verified entries produce enabled skip plans and that budgets/oversize/malicious cases fall back safely.
tests/e2e/app-router/build-id-navigation.spec.ts Adds Playwright coverage proving RSC soft navigations send a bounded client reuse manifest header.
tests/app-rsc-handler.test.ts Verifies parsed client reuse manifest hints are threaded from canonical RSC requests into page dispatch.
tests/app-rsc-cache-busting.test.ts Ensures manifest header is attached without becoming a shared cache-variant input.
tests/app-router.test.ts Asserts generated dispatchMatchedPage wiring includes clientReuseManifest threading.
tests/app-page-render.test.ts Adds render lifecycle coverage for skip omission, no-store cache policy, and HTML non-omission behavior.
tests/app-page-dispatch.test.ts Adds dispatch-boundary integration coverage for verified static-layout omission (RSC only) and cache bypass.
tests/app-elements.test.ts Validates outgoing payload builder omits only layout keys when skip is enabled and preserves metadata/layout flags.
tests/app-browser-client-reuse-manifest.test.ts New tests for browser manifest construction (static-only, retained-only) and byte-budget trimming.
packages/vinext/src/server/static-layout-client-reuse-proof.ts Introduces shared proof helpers for static-layout compatibility shaping + payload hash computation.
packages/vinext/src/server/skip-cache-proof.ts Adds skip transport planner (createClientReuseSkipTransportPlan) and upgrades verified cross-checks to enabled skip disposition.
packages/vinext/src/server/client-reuse-manifest.ts Extends skip disposition type to include SKIP_STATIC_LAYOUT_VERIFIED (enabled) alongside disabled mode.
packages/vinext/src/server/app-rsc-handler.ts Threads clientReuseManifest through handleAppRscRequest to dispatch boundary.
packages/vinext/src/server/app-rsc-cache-busting.ts Allows createRscRequestHeaders() to attach the manifest header when provided.
packages/vinext/src/server/app-page-render.ts Builds runtime skip disposition at render lifecycle boundary; omits proven layouts from RSC payload and bypasses RSC cache writes (no-store).
packages/vinext/src/server/app-page-dispatch.ts Threads clientReuseManifest through dispatch into render lifecycle.
packages/vinext/src/server/app-elements-wire.ts Applies skip disposition during outgoing payload encoding by filtering proven layout keys only.
packages/vinext/src/server/app-browser-entry.ts Attaches the client reuse manifest header to real navigation RSC fetches (prefetch unchanged).
packages/vinext/src/server/app-browser-client-reuse-manifest.ts New browser helper to construct a bounded manifest from visible AppElements metadata.
packages/vinext/src/entries/app-rsc-entry.ts Updates generated entry wiring to pass clientReuseManifest into app page dispatch/render.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/vinext/src/server/app-page-render.ts
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 58dfd28128

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/vinext/src/server/app-browser-entry.ts
Skip transport sends partial RSC payloads only for clients that already hold retained static layouts. Treating those payloads as restorable visited-cache entries, sending manifests on refresh, or skipping layouts marked dynamic by runtime segment config breaks that invariant.

The previous wiring relied on generic payload metadata and build-time classifications, while hard-navigation recovery had drifted around the navigation controller guard.

Record skipped layout ids in AppElements metadata, deny visited-cache restoration for skip-pruned payloads, preserve skipped layouts from current state on approved commits, restrict browser manifests to navigate requests, honor runtime dynamic segment config, and route hard navigations through the controller.
Compatibility-map acceptance lets a retained layout hint verify across compatible canary and rollback deployments, but it does not prove byte-for-byte transport identity. Treating that accepted compatibility as permission to omit layout bytes breaks the cache proof boundary when deployments only share a declared compatibility set.

Keep the cross-check verified in that case, but return the disabled skip disposition unless the manifest entry compatibility exactly matches the current artifact compatibility. Add regression coverage for shared and stale canary/rollback compatibility maps.
…05-static-layout-skip-transport

# Conflicts:
#	tests/skip-cache-proof.test.ts
@NathanDrake2406 NathanDrake2406 marked this pull request as ready for review May 22, 2026 16:00
@NathanDrake2406
Copy link
Copy Markdown
Contributor Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b04d9b7273

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/vinext/src/server/app-page-render.ts
@james-elicx
Copy link
Copy Markdown
Member

/bigbonk review

Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: feat(skip): enable proven static layout transport skips

I read the full diff, the #726 architectural spec, and the prior reviews from Bonk, Copilot, and Codex. The architecture is well-designed and the implementation is disciplined. I'm focusing on areas the other reviews didn't cover or where I disagree with their findings.

Correctness assessment

The end-to-end trust boundary is sound:

  1. Parse-time disposition stays disabled. parseClientReuseManifestHeader always returns enabled: false. Only createClientReuseSkipTransportPlan at render time can upgrade to SKIP_STATIC_LAYOUT_VERIFIED.
  2. Cache bypass is complete. shouldCaptureRscForCacheMetadata is false, capturedRscDataPromise is null, waitUntil is never called, and no-store, must-revalidate is set.
  3. Visited-cache poisoning is prevented. isCacheRestorableAppPayloadMetadata returns false when skippedLayoutIds.length > 0 (line 157 of app-browser-state.ts), so skip-pruned payloads are never stored. The Codex P1 comment about visited-route cache keying is a false positive — the store path is gated off entirely.
  4. Layout-only omission guard is structurally correct. createSkippedLayoutIds re-parses through parseAppElementsWireElementKey and only admits kind: "layout".
  5. Render dependency release on skip is correct. releaseAppElementRenderDependency is called for skipped entries so downstream renderAfterAppDependencies barriers don't hang.
  6. CDN cache coherence is correct. The manifest header is intentionally excluded from createCacheBustingInput and VINEXT_RSC_VARY_HEADER, so CDN caches are not manifest-dimensioned. A CDN-cached full response is harmlessly served to manifest-bearing clients.

Param scope design is the strongest part

The structural param scope tracking (via resolveAppPageSegmentParamScopeKeys) is the right conservative approach. It blocks skip for any layout whose ancestor segments include dynamic, catch-all, or optional catch-all params — regardless of whether the layout component actually reads them. This is correct because:

  • A layout under [slug] could receive params and pass them to children without reading them itself
  • The probe-time observation only catches direct reads, not pass-through
  • Optional catch-all segments whose runtime value is []/{} still have non-empty structural scope

The three-gate rejection order in rejectParamDependentStaticLayout (incomplete observation → non-empty scope → observed reads) is clean and each gate has a distinct rejection code for traceability.

On the Codex P2 concern about synthetic observations

The second Codex comment about cacheTags: [] and dynamicFetches: [] in synthetic observations raises a real concern worth tracking. A layout that does await fetch(url, { next: { tags: ['config'] } }) without calling request APIs would be classified "s" at probe time, but revalidateTag('config') would change its output. The synthetic observation with cacheTags: [] means the skip proof doesn't know about the config tag. This is a known scope limitation for this PR (no tag-based invalidation tracking for skip proof), but it should be tracked as a future correctness gap.

Items below — all non-blocking refinements

See inline comments.

Comment thread packages/vinext/src/server/app-layout-param-observation.ts
Comment thread packages/vinext/src/server/app-page-render.ts
Comment thread packages/vinext/src/server/app-browser-client-reuse-manifest.ts
Comment thread packages/vinext/src/server/app-page-dispatch.ts
Comment thread packages/vinext/src/server/app-page-render.ts
Comment thread packages/vinext/src/entries/app-rsc-entry.ts Outdated
Comment thread packages/vinext/src/server/app-browser-state.ts
Static-layout skip could over-share or under-observe probe dependency state. Fetch tags, cacheable fetch URLs, dynamic fetch URLs, and cacheLife were read from request-wide collections, so dependencies from one layout probe could be attributed to later probes.

The violated invariant was that skip eligibility must be proven from the layout subtree being considered, not from neighbouring probes or from wrapper shapes the probe cannot see. Isolate each layout probe's dependency observation in a child request scope, render through React.lazy wrappers, and count array and iterable branches against the existing subtree depth and node budgets.

Tests cover per-layout dependency isolation, lazy cache-tagged layout children preserving the RSC layout record, and array depth and node limit enforcement.
@NathanDrake2406
Copy link
Copy Markdown
Contributor Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6de623645a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/vinext/src/server/app-browser-client-reuse-manifest.ts
Browser soft navigation could emit more retained layout entries than the server skip planner will verify. When a valid manifest exceeded that verification budget, the server rejected the whole hint with SKIP_ENTRY_COUNT_EXCEEDED and skip transport stayed disabled for routes with many retained static layouts.

The mistaken invariant was treating the parser's hostile-input entry limit as the producer budget. Keep the parser envelope larger, share the server verification budget through the manifest protocol module, and cap browser-produced manifests to that budget before byte-size trimming.

The regression test builds a route with more retained static layouts than the verification budget and asserts the browser manifest only contains the verifiable prefix.
@NathanDrake2406
Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. You're on a roll.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Static-layout skip could still authorize omission after a layout probe observed request APIs. Build-time or segment-config static classification can skip isolated dynamic detection, so a retained layout that reads headers(), cookies(), draftMode(), connection(), params, or search params needed an independent runtime negative proof before bytes could be omitted.

The violated invariant was that skip proof must be based on evidence owned by the per-layout observation tracker, not a fabricated empty request API observation. Record request API usage inside each isolated layout probe, reject observed request APIs before static-layout proof construction, and feed the completed per-layout request API observation into the synthetic cache proof.

Tests cover isolated request API tracking across layout probes and a dispatch regression where a static-classified layout probe observes headers usage and the RSC payload still includes the layout.
@NathanDrake2406
Copy link
Copy Markdown
Contributor Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1863f07ab5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/vinext/src/server/app-page-probe.ts Outdated
Layout subtree probing consumed arbitrary iterable children before the real RSC render. If an app returned a single-use iterator or shared generator from a layout subtree, the probe could advance it and leave the real render with missing children.

The mistaken invariant was treating all iterables like arrays. Arrays are stable values to traverse, but arbitrary iterables can be stateful, so the probe now rejects non-array iterables without advancing them. That keeps the real render authoritative and makes skip fall back instead of mutating output.

The regression verifies a shared generator is still unconsumed after the subtree probe rejects it.
@NathanDrake2406
Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. You're on a roll.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Layout probes could record unsafe static-reuse observations while still publishing an "s" layout flag. That was safe for server skip authorization, but it let the browser keep proposing manifest entries for layouts that could never be accepted.

The violated invariant was that client-facing layout flags should be derived from the same per-layout evidence used by skip authorization. Treat incomplete observations, structural params, observed params, request APIs, finite revalidate, cacheLife, cache tags, cacheable fetches, and dynamic fetches as dynamic for layout metadata.

Tests cover both build-time-static false positives and runtime-probed request API layouts publishing "d" while still rendering and sending the layout.
@NathanDrake2406
Copy link
Copy Markdown
Contributor Author

@codex review

Layout skip probes rendered layouts with children set to null. That could hide layout chrome dependencies behind children truthiness checks, producing a clean static observation for a layout whose real render would observe request or data dependencies.

The violated invariant was that per-layout skip evidence must be gathered under a child shape equivalent to the real App Router render. Probe layouts now receive a terminal non-null React child, while the subtree probe treats it as inert instead of descending into page content.

This also restores build-time layout flags before special-error probes so short-circuit responses keep the same metadata contract covered by CI. Tests cover children-dependent chrome observations and the special-error flag regression.
@NathanDrake2406
Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🎉

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Static layout skip could treat layouts that read unstable_cache() data as clean. That was unsafe because cached data can be tagged, revalidated, and invalidated without passing through the fetch, cacheLife, request API, or params observation channels.

The violated invariant was that skip proof must observe every layout data dependency source before synthesizing a static-layout proof. unstable_cache() now records a bounded observation for every invocation, including cache hits, using hashed keys and tag metadata rather than raw keys or values.

Layout observation carries those dependencies into skip rejection and client-visible layout flags. Tests cover cache-miss and cache-hit observation plus dispatch fallback for a retained static layout whose probe reads unstable_cache() data.
@NathanDrake2406
Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Hooray!

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@james-elicx
Copy link
Copy Markdown
Member

/bigbonk review

Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: feat(skip): enable proven static layout transport skips

I read the full diff (4154 additions, 43 files), the #726 architectural context, and all prior review threads from Bonk, Copilot, and Codex. The overall architecture is strong — the trust boundary is clean, the cache bypass is complete, and the observation isolation design is well-thought-out. The test coverage is thorough (7500+ lines of new/modified tests across 15 files).

Correctness summary

Trust boundary — Parse-time disposition stays renderAndSend (disabled). Only createClientReuseSkipTransportPlan at render time can upgrade to SKIP_STATIC_LAYOUT_VERIFIED. The skippedEntryIds array is populated from entry.id (canonical parsed manifest entry), not from verifier-returned IDs, preventing injection. The "uses the verified manifest entry id instead of trusting verifier-provided skipped ids" test in skip-cache-proof explicitly covers this.

Cache bypass — Complete end-to-end. When skip is enabled: shouldCaptureRscForCacheMetadata is false, capturedRscDataPromise is null, waitUntil is never called, no-store, must-revalidate is set, and the absence of cacheState in rscResponsePolicy correctly causes preserveClientResponseHeaders: true in finalizeAppPageRscCacheResponse, preventing the no-store header from being overwritten.

Visited cache — Skip-pruned payloads carry skippedLayoutIds in metadata. isCacheRestorableAppPayloadMetadata returns false when skippedLayoutIds.length > 0 (line 157 of app-browser-state.ts), gating off the visited-response store path entirely. Codex P1 about visited cache keying was a false positive, as the author and Bonk both confirmed.

Observation isolationrunWithUnifiedStateMutation creates fresh cacheableFetchUrls, currentRequestTags, dynamicFetchUrls, dynamicUsageDetected, renderRequestApiUsage, requestScopedCacheLife, and unstableCacheObservations containers per layout probe. Dependencies are recorded via recordProbeDependencies inside the isolated scope before it exits. Codex P2 and Copilot correctly identified the pre-isolation accumulation bug; the fix in 6de6236 is sound.

Render dependency releasereleaseAppElementRenderDependency is correctly called in buildOutgoingAppPayload for skipped entries, preventing downstream page/layout render barriers from deadlocking. The WeakMap-based registration in app-page-route-wiring.tsx and release in app-elements-wire.ts form a clean lifecycle.

Subtree probe — Handles function components, React.memo, React.forwardRef, and React.lazy (via thrown-promise resolution). Array branches correctly count against depth/node limits. Non-array iterables are rejected without advancing them. The wrapperDepth counter prevents infinite loops through nested wrapper chains.

Items below — all non-blocking

See inline comments for specific code-level observations.

},
(error: unknown) => {
recordProbeDependencies(layoutId);
throw error;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error path records dependencies (recordProbeDependencies) but does not call markProbeComplete, leaving the observation at completeness: "unknown". This is the correct conservative outcome (blocks skip via SKIP_LAYOUT_PARAMS_OBSERVATION_INCOMPLETE), as discussed in the prior Bonk review thread.

However, there's a subtlety worth noting: recordProbeDependencies on the error path means the observation will contain whatever fetch tags/cache reads/etc the layout accumulated before it threw. These partial dependency snapshots are never used for skip authorization (since completeness blocks it), but they are surfaced in the rejection's fields object via rejectStaticLayoutObservation. This is fine for diagnostics — just noting it's not dead data in the trace output.

if (value == null || typeof value === "boolean" || typeof value === "number") return;
if (typeof value === "string" || typeof value === "bigint") return;
if (isPromiseLike(value)) {
await visit(await value, depth);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awaited promises don't increment depthawait visit(await value, depth) re-visits at the same depth as the promise itself. This means a chain of nested promises (Promise.resolve(Promise.resolve(Promise.resolve(<Component />)))) would visit N+1 nodes without increasing depth, potentially allowing deeper-than-maxDepth effective nesting through promise chains.

In practice, React doesn't produce arbitrarily nested promise chains in server component output, and the maxNodes counter still bounds the total work. But if you want the depth invariant to be airtight, this could use depth + 1.

Non-blocking — the node limit catches any real abuse.

pathTags: [input.cleanPathname],
requestApis: buildRenderRequestApiObservations({
completeness: "complete",
observed: input.layoutParamAccess?.getLayoutObservation(entry.id).requestApis ?? [],
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The requestApis field in the synthetic observation reads from layoutParamAccess?.getLayoutObservation(entry.id).requestApis ?? []. But by this point, rejectUnsafeStaticLayoutObservation (line 447) has already verified that observation.requestApis.length === 0 — otherwise the function would have returned an early rejection at line 342. So this will always be [] when it reaches here.

Not a bug, but this creates a subtle coupling: the correctness of the synthetic observation depends on the ordering of rejection checks in rejectUnsafeStaticLayoutObservation. If someone adds a new rejection check between the request API check and the synthetic observation, and that new check somehow passes despite request APIs being present, the synthetic observation would carry stale data.

Consider either:

  • Hardcoding requestApis: buildRenderRequestApiObservations({ completeness: "complete", observed: [] }) with a comment explaining the guarantee
  • Or adding an assertion: assert(observation.requestApis.length === 0)

Non-blocking.

treePosition,
options.matchedParams,
);
return probeReactServerSubtree(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The probe renders with probeReactServerSubtree(<LayoutComponent ... >{APP_PAGE_LAYOUT_PROBE_CHILD}</LayoutComponent>) where APP_PAGE_LAYOUT_PROBE_CHILD = <Fragment />. This is a solid approach — it exercises the children-truthy branch in layouts that conditionally render chrome based on whether children are present.

One nuance: if a layout destructures children and checks its type (e.g., if (typeof children.type === 'string')), the Fragment child would behave differently than a real page component child. This is an unlikely pattern in real layouts, and the probe would still correctly observe any data dependencies triggered by the truthy-children branch, so skip safety is maintained. Just noting the semantic gap.

): ClientReuseManifestLimits {
return {
...limits,
maxEntryCount: Math.min(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good design: capClientReuseManifestProducerLimits ensures the browser never produces more entries than the server's verification budget (CLIENT_REUSE_MANIFEST_SKIP_VERIFICATION_ENTRY_BUDGET = 8). This prevents the guaranteed SKIP_ENTRY_COUNT_EXCEEDED rejection that Codex P2 identified.

The limit is applied as Math.min(limits.maxEntryCount, budget) — this means if the default limit (64) is larger than the budget (8), the budget wins. If someone configures a custom limit below 8, that custom limit wins. Both directions are correct.

return {
kind: "dynamic",
reason: { layer: "segment-config", key: "dynamic", value: "force-dynamic" },
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good addition: classifyLayoutSegmentConfigFromModule reads dynamic and revalidate exports from layout modules at runtime. This closes the gap where a layout with export const dynamic = 'force-dynamic' at the module level wouldn't be reflected in build-time classifications.

Note that createEffectiveLayoutClassifications merges these runtime classifications by overriding build-time ones (line 285: classifications.set(index, classification.kind)). This means a runtime force-static will override a build-time dynamic classification. I verified this is the correct precedence — segment config exports are the developer's explicit intent and should win over build-time heuristic analysis.

releaseAppElementRenderDependency(input.element, key);
continue;
}
payload[key] = value === UNMATCHED_SLOT ? APP_UNMATCHED_SLOT_WIRE_VALUE : value;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The iteration-based approach (replacing the previous spread) is cleaner and enables skip filtering in a single pass. The UNMATCHED_SLOTAPP_UNMATCHED_SLOT_WIRE_VALUE conversion that previously happened somewhere else is now handled inline here.

One observation: the ordering of keys in the output payload is now:

  1. Non-skipped element entries (in source iteration order)
  2. __layoutFlags
  3. __skippedLayoutIds (if non-empty)
  4. __artifactCompatibility
  5. __cacheEntryReuseProof (if present)
  6. __renderObservation (if present)

Previously, the spread put all entries first with __layoutFlags and __artifactCompatibility interleaved. The new order places metadata keys at the end. This shouldn't affect RSC serialization (Flight doesn't depend on key order), but it's a subtle change in wire format that downstream snapshot tests might notice. The existing tests passing confirms this is fine.

Comment on lines 420 to +424
);
}
const errorResponse = await probeLayoutForErrors(options, layoutIndex);
if (errorResponse) return errorResponse;
const observationDynamic = cls.isLayoutObservationDynamic?.(layoutId) === true;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important correctness detail: for build-time classified layouts, the observation check happens after the special error probe (line 421: const errorResponse = await probeLayoutForErrors(...)). This means:

  1. Layout is probed for redirects/not-found errors
  2. If no special error, observation dynamic check runs
  3. Layout flag is set to "d" if build-time said dynamic OR observation said dynamic

The flag assignment at line 411 (layoutFlags[layoutId] = buildTimeResult === "static" ? "s" : "d") happens before the error probe, and then gets overwritten at line 423. The intermediate value at line 411 is never read by anything between lines 411 and 423 because the error probe doesn't consult layout flags.

This two-step assignment is slightly confusing but correct. Consider combining into a single assignment after the observation check if you want to clean this up later.

const value = Reflect.get(target, prop, receiver);
if (typeof value !== "function") return value;
return (...args: unknown[]) => {
observeAllParamKeys(observer, plain);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When await params triggers the then handler, observeAllParamKeys reports ALL param keys — not just the ones the layout destructures after awaiting. This is the correct conservative choice: const { slug } = await params desugars to calling .then() and then destructuring the resolved value, so the observer only sees the .then() call, not which properties are later read from the resolved object.

Worth noting: this means await params always reports the full param set as observed, which will trigger SKIP_LAYOUT_PARAMS_OBSERVED rejection in rejectUnsafeStaticLayoutObservation even if the layout only reads one param. Combined with the structural param scope gate (SKIP_LAYOUT_PARAMS_PRESENT), any layout under a dynamic segment that touches params at all — even via await — will correctly never be skip-eligible.

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented May 26, 2026

Review submitted successfully with 9 inline comments. Here's a summary:

Overall: The PR is solid. The architecture correctly preserves the #726 trust boundary design end-to-end. All 9 inline comments are non-blocking refinements.

Key correctness findings:

  1. Trust boundary is clean — parse-time disposition stays disabled; only createClientReuseSkipTransportPlan at render time can upgrade to SKIP_STATIC_LAYOUT_VERIFIED. The skippedEntryIds are derived from canonical manifest entry IDs, not verifier output.

  2. Cache bypass is completeshouldCaptureRscForCacheMetadata is false, ISR writes are skipped, no-store, must-revalidate is set, and the intentional absence of cacheState correctly preserves the no-store header through finalization.

  3. Visited cache poisoning is preventedisCacheRestorableAppPayloadMetadata returns false when skippedLayoutIds.length > 0, gating off the store path entirely. Codex P1 was a false positive.

  4. Observation isolation is correctrunWithUnifiedStateMutation creates fresh containers per layout probe, preventing cross-layout dependency bleeding.

  5. Render dependency release prevents deadlocksreleaseAppElementRenderDependency is called for skipped entries in buildOutgoingAppPayload, so downstream pages don't block waiting for omitted layouts.

Inline comments covered: partial dependency snapshots in error-path observations, promise-chain depth counting in subtree probe, redundant requestApis field in synthetic observation, Fragment probe child semantics, producer budget cap design, segment config classification precedence, payload key ordering change, two-step layout flag assignment in probeAppPageLayouts, and await params conservative observation behavior.

github run

@NathanDrake2406
Copy link
Copy Markdown
Contributor Author

Bonk: pls approve the PR if there's no more issues

The static export test times out at the default 30s in CI due to the
additional probeReactServerSubtree walk introduced by static layout
transport skip verification. The probe walks the component tree to
detect dynamic data dependencies, adding overhead that pushes the test
past 30s on CI's slower hardware. Increase to 60s.
@james-elicx
Copy link
Copy Markdown
Member

/bigbonk review

Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: feat(skip): enable proven static layout transport skips

I read the full diff (4155 additions across 43 files), the #726 architectural context, all prior review threads, and the source code of every module touched by this PR. The implementation is correct and well-designed. Approving with a few non-blocking observations below.

Architecture assessment

The trust boundary design is the strongest aspect of this PR. The layering is disciplined:

  1. Parse timeparseClientReuseManifestHeader always returns enabled: false. No parser-level trust of client input.
  2. Render timecreateRenderLifecycleSkipDisposition is the sole upgrade path to SKIP_STATIC_LAYOUT_VERIFIED, gated behind a 10-check observation pipeline (rejectUnsafeStaticLayoutObservation).
  3. Encoding timebuildOutgoingAppPayload re-validates layout-kind via parseAppElementsWireElementKey before admitting any ID to the skip set. Defense in depth.
  4. Client timeisCacheRestorableAppPayloadMetadata gates off visited-cache storage when skippedLayoutIds.length > 0, and mergeSkippedLayoutPreservation can only add to preserveElementIds, never remove.

Cache bypass is complete

  • shouldCaptureRscForCacheMetadata is false when skip is active (line 717)
  • capturedRscDataPromise is null (passed as null at line 793)
  • no-store, must-revalidate is set via NO_STORE_CACHE_CONTROL (line 746)
  • Missing cacheState in rscResponsePolicy correctly causes preserveClientResponseHeaders: true at line 821, preventing finalization from overwriting the no-store header
  • ISR write is skipped (the waitUntil path is never reached)
  • ISR debug log is emitted (line 758)

Observation isolation is sound

runWithIsolatedProbeDependencies creates a child unified request context via runWithUnifiedStateMutation, replacing all 7 accumulator fields with fresh containers. The recordProbeDependencies call at the end of each probe reads from the child scope's containers (within the ALS continuation), so parent/sibling layout observations are not contaminated. The fix in 6de6236 correctly resolved the cross-layout accumulation bug that Codex and Copilot identified.

Render dependency lifecycle is correct

When a layout is skipped in buildOutgoingAppPayload, releaseAppElementRenderDependency is called (line 634 of app-elements-wire.ts), which resolves the promise that downstream renderAfterAppDependencies barriers await. Without this, skipped layouts would cause downstream page/layout renders to deadlock. The WeakMap-based registration ensures the release is scoped to the element record's lifetime.

Non-blocking observations

See inline comments.

if (value == null || typeof value === "boolean" || typeof value === "number") return;
if (typeof value === "string" || typeof value === "bigint") return;
if (isPromiseLike(value)) {
await visit(await value, depth);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Promise-chain depth: await visit(await value, depth) revisits at the same depth rather than depth + 1. This means a chain of nested promises doesn't increment depth, so Promise.resolve(Promise.resolve(Promise.resolve(<Component />))) traverses N+1 nodes at the same depth level.

This is fine in practice — React server component output doesn't produce arbitrarily nested promise chains, and the maxNodes counter still bounds total work. But if you ever want the depth invariant to be airtight against synthetic inputs, this could use depth + 1 for the outer visit.

Non-blocking — noted for completeness, maxNodes catches real abuse.

return cacheLife === null ? null : JSON.stringify(cacheLife);
};

const runWithIsolatedProbeDependencies = (probe: () => unknown): unknown => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The isInsideUnifiedScope() guard at line 123 means that when layout probes run outside a unified request context (e.g., in certain test harnesses or edge cases), the probe executes without isolation — dependencies accumulate on the request-wide scope. This is the correct fallback (no isolation = conservative accumulation = more rejections = safe), but worth noting that tests exercising probe isolation must run inside a unified scope to get meaningful coverage.

The existing test suite does this correctly via the dispatch helper setup.

pathTags: [input.cleanPathname],
requestApis: buildRenderRequestApiObservations({
completeness: "complete",
observed: input.layoutParamAccess?.getLayoutObservation(entry.id).requestApis ?? [],
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This reads layoutParamAccess?.getLayoutObservation(entry.id).requestApis ?? [] for the synthetic observation, but by this point rejectUnsafeStaticLayoutObservation (line 342) has already verified requestApis.length === 0 — otherwise we'd have returned at line 343. So this will always be [].

The code is correct, but the dependency on rejection check ordering is implicit. If someone later reorders the rejection checks in rejectUnsafeStaticLayoutObservation and a new check passes despite request APIs being present, this synthetic observation would carry unexpected data.

Consider either hardcoding observed: [] with a comment explaining the invariant, or adding a debug assertion. Non-blocking — the ordering dependency is stable and well-tested.

limits: ClientReuseManifestLimits;
visibleCommitVersion: number;
}): string | null {
const entries = input.entries.slice(0, input.limits.maxEntryCount);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The binary search for bounded serialization is clean. One edge case worth noting: if entries.length === 0 after the slice(0, maxEntryCount), the while (low <= high) loop never executes (low=1 > high=0) and best stays null. This is correct — the caller checks entries.length === 0 before calling this function. But if this function were ever called directly with an empty entries array, it would return null rather than an empty manifest. The current call site at line 165 is correctly guarded.

Non-blocking — just documenting the contract.

const layoutModule = options.route.layouts[options.layoutIndex];
const LayoutComponent = getDefaultExport(layoutModule);
if (!LayoutComponent) return null;
recordLayoutSkipObservationScope({
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good: recordLayoutSkipObservationScope runs inside the probe closure, which means it executes within the runLayoutProbe isolation scope. This ensures the param scope and finite revalidate recording happen after the isolated context is established but before the layout component is invoked.

One subtlety: recordLayoutSkipObservationScope only reads static module exports (revalidate) and route segment structure — it doesn't invoke any async code or trigger data fetches. So it wouldn't actually be affected by the isolation scope. But placing it inside the probe closure is still the right choice for maintainability — it keeps all per-layout observation recording in one place.

}
const errorResponse = await probeLayoutForErrors(options, layoutIndex);
if (errorResponse) return errorResponse;
const observationDynamic = cls.isLayoutObservationDynamic?.(layoutId) === true;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The two-step layout flag assignment (line 411: set from build-time, line 427: potentially override from observation) works correctly because nothing between lines 411 and 427 reads layoutFlags[layoutId]. The error probe at line 421 doesn't consult layout flags.

But if a future change to probeLayoutForErrors ever needed to read layout flags (e.g., to condition error handling on static/dynamic classification), the intermediate "s" value from line 411 could be incorrect for a layout whose observation says dynamic. Consider collapsing into a single assignment after the observation check:

Suggested change
const observationDynamic = cls.isLayoutObservationDynamic?.(layoutId) === true;
const errorResponse = await probeLayoutForErrors(options, layoutIndex);
if (errorResponse) return errorResponse;
const observationDynamic = cls.isLayoutObservationDynamic?.(layoutId) === true;
layoutFlags[layoutId] = buildTimeResult === "dynamic" || observationDynamic ? "d" : "s";

Non-blocking — the current code is correct for the current call graph.

releaseAppElementRenderDependency(input.element, key);
continue;
}
payload[key] = value === UNMATCHED_SLOT ? APP_UNMATCHED_SLOT_WIRE_VALUE : value;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The UNMATCHED_SLOTAPP_UNMATCHED_SLOT_WIRE_VALUE conversion is now handled inline during the iteration loop. Previously this conversion was done by the spread operator implicitly (since the element values were already converted before being stored). Verify that UNMATCHED_SLOT symbols can actually appear as values in input.element at this point — if they're already converted upstream, this check is dead code.

Looking at buildAppPageElements in app-page-route-wiring.tsx, UNMATCHED_SLOT symbols are indeed stored as element values for unmatched parallel route slots. So this conversion is necessary and correct.

Non-blocking observation — the inline conversion is cleaner than the previous implicit approach.

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented May 28, 2026

Review submitted successfully. Here's a summary:

Approved the PR with 7 non-blocking inline observations.

Key findings:

The implementation is correct. The trust boundary design is disciplined across all four layers (parse → render → encode → client). The cache bypass is complete end-to-end. The observation isolation via runWithUnifiedStateMutation is sound. The render dependency release for skipped layouts prevents deadlocks.

Inline comments covered:

  1. Promise-chain depth invariant in subtree probe (cosmetic)
  2. Isolation scope fallback behavior outside unified request context
  3. Implicit ordering dependency between rejection checks and synthetic observation construction
  4. Binary search empty-entries edge case documentation
  5. Probe closure placement of recordLayoutSkipObservationScope
  6. Two-step layout flag assignment could be collapsed for future-proofing
  7. UNMATCHED_SLOT conversion correctness verification in the new iteration loop

github run

…05-static-layout-skip-transport

# Conflicts:
#	packages/vinext/src/server/app-page-element-builder.ts
#	tests/app-page-dispatch.test.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

App Router layout persistence: Route Manifest and Navigation Planner architecture

3 participants