Skip to content

fix(app-router): vary segment prefetches by accessed params - #2861

Draft
james-elicx wants to merge 13 commits into
mainfrom
codex/fix-segment-vary-params
Draft

fix(app-router): vary segment prefetches by accessed params#2861
james-elicx wants to merge 13 commits into
mainfrom
codex/fix-segment-vary-params

Conversation

@james-elicx

Copy link
Copy Markdown
Member

Summary

  • learn page, layout, metadata, search-param, and root-param dependencies from completed prefetch renders
  • replay learned segment payloads only when the route, page element, Suspense boundary, and accessed params match
  • preserve optional catch-all and inherited named-slot identities, including slot-effective params and client cache keys
  • keep optimistic routing and flattened parallel-slot ordinals aligned with the server render

Original failures

This addresses the 11 failures for test/e2e/app-dir/segment-cache/vary-params/vary-params.test.ts reported by deploy-suite run 31290819291:

  1. renders cached loading state instantly during navigation
  2. renders cached loading state instantly with runtime prefetching
  3. does not reuse prefetched segment when page accesses searchParams
  4. reuses prefetched segment when page does not access searchParams
  5. tracks param access in generateMetadata
  6. caches head segment when generateMetadata does not access params
  7. reuses page segment when layout varies but page does not
  8. shares cached segment across all params when none accessed statically (runtime prefetch)
  9. shares cached segment across search params when not accessed (runtime prefetch)
  10. tracks vary params per-segment with layout/page split (runtime prefetch)
  11. tracks root param access via rootParams API

Validation

  • vp test run expanded focused set: 820/820 on the independently reviewed head
  • vp check: clean
  • exact Next.js targeted runner: 16/16 twice
REPO="$(pwd)" \
NEXTJS_DIR="/Users/jamesanderson/Developer/vinext/.nextjs-ref" \
NEXT_TEST_CONCURRENCY=1 \
./scripts/run-targeted-nextjs-e2e.sh \
test/e2e/app-dir/segment-cache/vary-params/vary-params.test.ts

Independent cumulative review of 8d336dd96b0d4bf133312c5354abf67abfe396a1: no findings.

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@pkg-pr-new

pkg-pr-new Bot commented Aug 10, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@vinext/cloudflare@2861
npm i https://pkg.pr.new/create-vinext-app@2861
npm i https://pkg.pr.new/@vinext/types@2861
npm i https://pkg.pr.new/vinext@2861

commit: 9dc32a5

@github-actions

Copy link
Copy Markdown
Contributor
Example Preview Production Original
app-router-cloudflare preview production
pages-router-cloudflare preview production
app-router-playground preview production original
realworld-api-rest preview production
nextra-docs-template preview production
benchmarks preview production
hackernews preview production original
workers-cache preview production
web preview production

@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Performance benchmarks

Compared 9dc32a5 against base d558d62 using alternating same-runner rounds. Next.js was unchanged and skipped.

0 improved · 5 regressed · 1 within ±1.5%

Scenario Framework Baseline Current Change
Client bundle size (gzip) vinext 135.2 KB 138.6 KB 🔴 +2.5%
Client entry size (gzip) vinext 122.7 KB 125.8 KB 🔴 +2.5%
Dev server cold start vinext 2.29 s 2.32 s ⚫ +1.0%
Production build time vinext 2.41 s 2.45 s 🔴 +1.9%
RSC entry closure size (gzip) vinext 114.2 KB 116.4 KB 🔴 +1.9%
Server bundle size (gzip) vinext 193.0 KB 196.1 KB 🔴 +1.6%

View detailed results and traces

🟢 improvement · 🔴 regression · ⚫ change below 1.5% · paired base/head

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk

ask-bonk Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

@james-elicx Bonk workflow was cancelled.

View workflow run · To retry, trigger Bonk again.

@ask-bonk ask-bonk Bot left a comment

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.

/bigbonk review — PR #2861 (vary segment prefetches by accessed params)

Reviewed all 40 changed files against main. The feature is well-contained behind the experimental triple-gate (cacheComponents && experimental.varyParams && experimental.optimisticRouting), so it defaults off and cannot affect existing users. Tests pass locally (vp check clean; app-prefetch-vary-analysis, app-page-probe, app-optimistic-routing, app-layout-param-observation, app-page-element-builder, app-rsc-cache-busting, thenable-params, rsc-completion-metadata, link, next-config, entry-templates — 610 tests green).

A few issues worth addressing before merge:

1. analyzeAppPrefetchCapabilities runs unconditionally at entry-generation time, even when the feature is disabled (perf)

toLinkPrefetchRoute (packages/vinext/src/entries/app-browser-entry.ts:119) calls analyzeAppPrefetchCapabilities(route) for every app route regardless of whether prefetch-vary is enabled. That helper does synchronous fs.readFileSync on the page source and every layout source (app-prefetch-vary-analysis.ts:38-52). For a large app this is repeated blocking file I/O on every dev rebuild and every production build, for output that is only consumed when the experimental flags are on.

generateBrowserEntry already threads prefetchVaryEnabled, but toLinkPrefetchRoute/toLinkPrefetchRoutes don't receive it (and index.ts:1522 calls toLinkPrefetchRoutes without it for the manifest). Consider gating the source analysis behind the flag so the default path stays free of extra I/O — this aligns with the AGENTS.md "keep the common request/build path lightweight" guidance.

2. Vary metadata can exceed the 4096-byte footer limit and error the RSC stream mid-flight

getPrefetchVaryMetadata() is serialized into the RSC completion footer via encodeFooter, which throws "RSC completion metadata exceeded its framing limit" when the payload exceeds MAX_FOOTER_BYTES (now 4096) — and this throw happens inside appendRscCompletionMetadata's pull() at stream end (rsc-completion-metadata.ts:96-98, 240-241), so it surfaces as a stream error that breaks the prefetch response rather than a clean degrade.

The validators are far more permissive than 4096 bytes: isStringArray allows 64 entries × 128 chars, and pageDynamicSuspenseOrdinalsByElementId allows 64 keys × 256 chars (rsc-completion-metadata.ts:22-43) — comfortably >4096 bytes for a route with many params / long element IDs. The updated test only bumps the size constant (tests/rsc-completion-metadata.test.ts) and doesn't cover an oversized vary payload.

Suggest either (a) bounding/truncating the metadata producer so it can never overflow the footer, or (b) making the emit path degrade gracefully (skip the footer) instead of erroring the stream, plus a test that drives a realistic oversized payload end-to-end. This is only reachable with the flags on, so it's not a release blocker, but it's a latent correctness bug for the feature's target users.

3. \0-delimited composite variant keys are theoretically ambiguous

runtimeTemplateVariantKey is built as ${sharedCacheKey}\0${loadingShellSharedCacheKey} (e.g. navigation.ts and link.tsx), but each operand already contains \0 internally (from resolveAppPrefetchSharedCacheKey, which joins pattern parts with \0). Concatenating two \0-containing strings with another \0 is not an injective encoding — ("a\0b","c") and ("a","b\0c") both yield "a\0b\0c". In practice both keys derive from the same URL+route so the split is deterministic, but it's a fragile invariant. Consider a length-prefixed or JSON.stringify([a, b]) join to make the composite unambiguous.

Minor / non-blocking

  • optimisticRouteTemplates now keys by variantKey in addition to route+context+slots, so a route hit with many distinct observed param values can accumulate more template entries. They're cleared on prefetch invalidation, but there's no per-route cap — matches existing prefetch-cache behavior, just noting the growth surface.
  • Monotonicity of learnAppPrefetchVaryMetadata looks correct (mergeVaryParamNames unions, ||= for booleans is monotonic). Good.
  • Dev/prod parity: this is App-Router-only and prod delegates to the built RSC entry, so no separate Pages-router prod path needs the change. Confirmed.

Overall the design is sound and the change is safely gated. #1 (unconditional build-time I/O) and #2 (stream-erroring footer overflow) are the two I'd want resolved before enabling the feature broadly.

@ask-bonk

ask-bonk Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2861.

Summary

I reviewed all 40 changed files in PR #2861. The feature (varying segment prefetches by accessed params) is well-architected and fully gated behind three experimental flags (cacheComponents && varyParams && optimisticRouting), so it defaults off and can't affect existing users. All relevant tests pass locally and vp check is clean.

I posted 3 substantive findings plus minor notes:

  1. Build-time perf regression (unconditional file I/O)analyzeAppPrefetchCapabilities does synchronous readFileSync on every page + layout source for every app route during entry generation, even when the feature is disabled. Should be gated behind prefetchVaryEnabled.

  2. Footer overflow can error the RSC stream — vary metadata is serialized into the 4096-byte RSC completion footer, but the validators permit payloads much larger than that (64×128-char param names, 64×256-char element IDs). Overflow throws inside the stream's pull(), breaking the prefetch response instead of degrading gracefully. No test covers an oversized vary payload.

  3. Ambiguous \0-delimited composite keysruntimeTemplateVariantKey concatenates two strings that each already contain \0, which isn't an injective encoding (theoretical collision).

Plus minor notes on template-map growth and confirmation that monotonicity and dev/prod parity are correct. Findings #1 and #2 are the two I flagged as worth resolving before enabling the feature broadly; neither is a release blocker since the feature defaults off.

github run

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

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.

/bigbonk review — PR #2861 (round 2, head 9730d999)

Re-reviewed after 9730d999 fix(cache): address vary prefetch review findings. The three findings from the prior review are handled:

  • #1 (unconditional build-time I/O) — fixed. toLinkPrefetchRoute/toLinkPrefetchRoutes now take prefetchVaryEnabled and skip analyzeAppPrefetchCapabilities when the feature is off (index.ts:1522 gates on cacheComponents && varyParams && optimisticRouting). Default path is I/O-free again. ✅
  • #3 (\0-delimited composite key ambiguity) — fixed. encodeAppPrefetchRuntimeTemplateVariantKey now uses JSON.stringify([a, b]), which is injective. All three call sites updated. ✅
  • #2 (footer overflow erroring the stream)changed, but the chosen fix introduces a silent correctness bug (see below).

1. Footer-overflow "graceful skip" silently under-varies the prefetch cache (correctness)

encodeFooter now returns undefined on overflow and the emit path drops the footer (rsc-completion-metadata.ts:93-107, 240-244). This removes the stream error, but for the vary-params feature it trades a loud failure for silent wrong-content caching.

The vary metadata is emitted in two places with different contents:

  • Header (app-page-render.ts:897-901) is set synchronously before the RSC body streams, so getPrefetchVaryMetadata() returns the observations known at response-start.
  • Footer (app-page-render.ts:909-913) is computed in the stream's pull() at end-of-render, so it captures the complete observation set — including params read deep inside Suspense boundaries (pageDynamicSuspenseOrdinalsByElementId, late pageParamNames). getPrefetchVaryMetadata() reads live mutable sets (app-layout-param-observation.ts:284-306), so the two snapshots genuinely differ.

The client prefers the footer and falls back to the header: extracted.metadata?.prefetchVary ?? decodePrefetchVaryHeader(...) (navigation.ts:1269-1271), and re-learns from the same source in the final learn step (navigation.ts:1480-1491). Learning is monotonic and additive (app-route-prefetch-policy.ts:174-185), and the default (no vary params learned) is permissiveresolveAppPrefetchSharedCacheKey treats every unmentioned param as a shared pattern placeholder (app-route-prefetch-policy.ts:225-230), i.e. "safe to reuse across all values".

Consequence: when the footer is dropped, the client silently loses the late-observed params and falls back to the pre-render header snapshot, which lacks exactly those params. It then treats them as non-varying and reuses one cached segment across different param values — the precise failure the feature exists to prevent. The overflow trigger (many params / long element IDs in pageDynamicSuspenseOrdinalsByElementId) is also exactly the payload the header snapshot can't carry, so the header fallback provides no protection here.

The new test (tests/rsc-completion-metadata.test.ts "omits oversized completion metadata…") asserts metadata: undefined and that the stream doesn't fail — i.e. it locks in the silent-drop behavior without checking that vary correctness survives.

Suggested direction (pick one):

  • Bound the producer so the footer can never overflow. Cap/truncate getPrefetchVaryMetadata() output to fit MAX_FOOTER_BYTES, but truncate toward the conservative side — dropping pageDynamicSuspenseOrdinalsByElementId granularity or collapsing to "vary by all params" rather than dropping param names. Then the fallback is stricter, not looser.
  • Or raise MAX_FOOTER_BYTES for the vary payload and keep the validators (isStringArray 64×128, isOrdinalRecord 64×256) in sync with a limit the payload can't exceed.
  • Either way, add a test that drives a realistic oversized vary payload end-to-end and asserts the client does not reuse across the affected params (correctness), not just that the stream survives.

This is only reachable with the experimental triple-gate on, so it's not a release blocker — but it's a latent correctness bug for the feature's target users, and the current fix makes it silent rather than loud.

Minor / non-blocking

  • parseFooterCandidate (rsc-completion-metadata.ts:114-116) and stripRscCompletionMetadata (:273-274) still throw/error on a >MAX_FOOTER_BYTES candidate, which is now unreachable from our own producer (it never emits one). That asymmetry is fine defensively, just noting the producer and consumer now disagree on whether oversize is fatal.
  • Growth surface unchanged from prior note: optimisticRouteTemplates keyed by variantKey can accumulate entries for routes hit with many distinct observed param values; cleared on prefetch invalidation, no per-route cap. Matches existing prefetch-cache behavior.
  • Monotonicity and dev/prod parity (App-Router-only; prod delegates to the built RSC entry) still hold.

Overall: findings #1 and #3 are cleanly resolved. The #2 fix removes the crash but should be reworked so the degradation is conservative (over-vary / stricter reuse) rather than permissive (reuse across params that actually vary).

@ask-bonk

ask-bonk Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2861.

Summary

I re-reviewed the head commit 9730d999 ("address vary prefetch review findings"), which addresses the three prior findings. Two are cleanly resolved; the third's fix introduces a new, subtler correctness bug.

Resolved:

New finding (#2 fix is incorrect):
The footer-overflow fix changed encodeFooter to silently return undefined and drop the footer instead of throwing. That removes the stream crash but is a latent correctness bug for the feature:

  • The header vary metadata is a pre-render snapshot (set before the body streams); the footer is the complete post-render set including params observed deep in Suspense boundaries.
  • The client falls back to the header when the footer is missing, and vary-learning is monotonic with a permissive default — unmentioned params are treated as shareable across all values.
  • So dropping the footer makes the client reuse one cached segment across param values that actually vary — exactly what the feature prevents. Worse, the overflow trigger (many params / long element IDs) is precisely the data the header snapshot lacks, so the fallback offers no protection.
  • The new test only asserts metadata: undefined + stream survival, locking in the silent-drop without verifying vary correctness.

I recommended reworking the degradation to be conservative (truncate toward "vary by all params" / stricter reuse) rather than permissive, or raising MAX_FOOTER_BYTES in sync with the validators, plus an end-to-end test that asserts no incorrect reuse. Not a release blocker (experimental triple-gate, defaults off), but it turns a loud failure into silent wrong-content caching for the feature's target users.

github run

@james-elicx

Copy link
Copy Markdown
Member Author

Exact-head deploy/CI audit for 9730d999ab11277bcd0d81ff6c3327ee95bcd8d5:

  • Deploy suite 31401982619 reports 2627 passed / 177 failed / 631 skipped, versus the nightly baseline's 2617 / 187 / 631.
  • All 11 intended vary-params assertions moved from fail to pass.
  • The report has exactly one pass-to-fail delta: segment cache (staleness) > reuses dynamic data up to the staleTimes.dynamic threshold.
  • I reran the exact upstream target at this PR head three times with scripts/run-targeted-nextjs-e2e.sh; that assertion passed 3/3. It also passes on clean current origin/main (056cd026) and on the PR's prior base (99623542). All of those controls reproduce only the suite's existing baseline failure, caches omitted from the prerender should not affect when the prefetch is expired. Verdict: the report-only stale-time delta is transient noise, not a regression from this PR.

The PR is mergeable with no conflict, so I did not merge/rebase/update its base. It must remain draft for now:

  1. CI 31401946254 is red in both browser-specific jobs because mismatching-prefetch.browser.spec.ts never observes the expected prefetch-loading-shell response. #2852 owns and fixes that exact behavior and remains open; CI needs a fresh synthetic-merge run after fix(app-router): recover mismatched prefetch rewrites #2852 lands.
  2. The latest Bonk run 31401982629 completed successfully as a workflow, but its posted review still contains one actionable correctness finding: silently dropping an oversized completion footer can fall back to permissive header metadata and incorrectly reuse a segment across parameter values. That finding needs resolution and a clean re-review before ready-for-review.

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

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.

/bigbonk review — PR #2861 (round 3, head 68f1163)

Re-reviewed after 68f1163 fix(app-router): fail closed on oversized vary metadata, which reworks the footer-overflow handling flagged in the round-2 review. The prior correctness finding is now resolved, and the fix is in the right (conservative) direction.

Prior finding #2 (footer overflow → silent under-varying) — resolved ✅

The previous fix silently dropped the footer on overflow, which fell back to the permissive pre-render header snapshot and could reuse one cached segment across param values that actually vary. The new commit replaces that with a fail-closed candidate ladder in both the header and footer encoders:

  1. Try the exact parsed metadata.
  2. If it doesn't fit, drop pageDynamicSuspenseOrdinalsByElementId but keep the flat pageDynamicSuspenseOrdinals union (a strict superset of every element's ordinals → over-invalidates, never under-invalidates).
  3. If still too big, emit a fixed-size conservative payload (varyAllParams: true, pageAllSuspenseDynamic: true, both search-param booleans true).

I verified the two new client flags actually enforce stricter reuse, not looser:

  • varyAllParamslearnAppPrefetchVaryMetadata mixes in getAllRouteParamNames(route) (all pattern + slot params) for every vary set and forces the search-param booleans (app-route-prefetch-policy.ts:178-199). Every param becomes value-keyed → no cross-value reuse.
  • pageAllSuspenseDynamic → threaded into replaceObservedDynamicSuspenseChildren(..., replaceAllSuspense=true), which replaces every Suspense boundary (and, at the depth-100 guard, the whole subtree) with OptimisticRouteSegment regardless of observed ordinals (app-optimistic-routing.ts:356, 499, 601). That forces re-fetch → the safe direction.

Both the header (app-page-render.ts:900) and the footer (:909-913) route through the same conservative encoders, so the two snapshots degrade consistently — closing the header/footer parity gap that made the old drop dangerous.

The non-null assertion in encodeFooter (rsc-completion-metadata.ts:212-214) is sound: the conservative payload plus the validated numeric/null scalar fields (dynamicStaleTimeSeconds, serverStaleTimeSeconds) are all fixed, bounded size, so it can never overflow MAX_FOOTER_BYTES.

Tests now assert correctness, not just stream survival ✅

The old test only checked metadata: undefined + stream survival. The new tests assert the content of the degraded metadata:

  • Oversized payload degrades to the conservative varyAllParams/pageAllSuspenseDynamic set (rsc-completion-metadata.test.ts "coarsens oversized completion metadata without failing or under-varying").
  • Oversized per-element map coarsens to the safe global union while preserving pageDynamicSuspenseOrdinals/pageParamNames.
  • Malformed and oversized headers fail closed to conservative metadata.
  • Sparse-array producer/wire inputs are rejected via the new isDenseArray guard.

All targeted suites pass locally (rsc-completion-metadata, app-optimistic-routing, app-prefetch-vary-analysis, app-page-probe, app-layout-param-observation, app-page-element-builder, app-rsc-cache-busting, thenable-params, link — 352 tests green), and vp check is clean on all changed files.

Producer invariant (defensive validator) — confirmed safe

The new isOrdinalRecordCoveredByGlobalUnion validator rejects a per-element map that references ordinals absent from the global union. getPrefetchVaryMetadata builds pageDynamicSuspenseOrdinals as the union of every element's ordinals by construction (app-layout-param-observation.ts:289-297), so legitimate producer metadata always satisfies the invariant — the check only guards against tampered wire data and never spuriously downgrades a valid payload.

Minor / non-blocking (unchanged from prior rounds)

  • parseFooterCandidate / stripRscCompletionMetadata still throw/error on a > MAX_FOOTER_BYTES candidate. This is now unreachable from our own producer (the encoder can no longer emit one), so it's purely defensive against corrupted input — fine to keep.
  • optimisticRouteTemplates keyed by variantKey can still accumulate entries for routes hit with many distinct observed param values; cleared on prefetch invalidation, no per-route cap. Matches existing prefetch-cache behavior.
  • Findings #1 (build-time I/O gating) and #3 (JSON.stringify composite keys) from prior rounds remain resolved.
  • Dev/prod parity holds (App-Router-only; prod delegates to the built RSC entry).

Verdict

The remaining actionable correctness finding from round 2 is resolved. The overflow path now degrades conservatively (over-vary / stricter reuse) rather than permissively, header and footer degrade in lockstep, and the new tests lock in the correct behavior. The feature remains fully gated behind the experimental triple-gate (cacheComponents && varyParams && optimisticRouting), so it defaults off. No blocking findings.

@ask-bonk

ask-bonk Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2861.

Summary

I re-reviewed the head commit 68f1163 ("fail closed on oversized vary metadata"), which reworks the footer-overflow handling that was the one remaining actionable finding from the round-2 review. That finding is now resolved, and the fix is in the correct (conservative) direction.

What changed and why it's correct:

The prior fix silently dropped the footer on overflow, falling back to the permissive pre-render header snapshot — which could reuse one cached segment across param values that actually vary (the exact bug the feature prevents). The new commit replaces that with a fail-closed candidate ladder applied to both the header and footer encoders:

  1. Try exact metadata.
  2. Drop the per-element ordinal map but keep the flat union (a strict superset → over-invalidates, never under-invalidates).
  3. Emit a fixed-size conservative payload (varyAllParams: true, pageAllSuspenseDynamic: true).

I verified both new flags enforce stricter reuse on the client:

  • varyAllParams → forces every route/slot param to be value-keyed (getAllRouteParamNames).
  • pageAllSuspenseDynamic → forces every Suspense boundary to OptimisticRouteSegment (re-fetch).

Both header and footer now route through the same conservative encoders, closing the parity gap. The tests were also upgraded from "assert metadata: undefined + stream survives" to asserting the actual conservative content, plus new coverage for malformed/oversized headers and a sparse-array (isDenseArray) guard.

Verification: 352 targeted tests green locally, vp check clean on all changed files. I also confirmed the producer always satisfies the new isOrdinalRecordCoveredByGlobalUnion validator by construction, so it never spuriously downgrades legitimate metadata.

Verdict: No blocking findings. Findings #1 and #3 from prior rounds remain resolved; #2 is now resolved. Feature stays fully gated behind the experimental triple-gate (defaults off).

github run

…y-params

# Conflicts:
#	packages/vinext/src/shims/link.tsx
#	packages/vinext/src/shims/navigation.ts
@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk

ask-bonk Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

UnknownError: ProviderInitError

github run

@ask-bonk

ask-bonk Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

@james-elicx Bonk workflow failed. Check the logs for details.

View workflow run · To retry, trigger Bonk again.

…y-params

# Conflicts:
#	packages/vinext/src/shims/root-params.ts
@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

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.

/bigbonk review — PR #2861 (round 4, head 93dbe24)

Independent full re-review of all 40 changed files at head 93dbe24 (merge of main into the feature branch; last feature commit 68f1163). The three actionable findings from prior rounds remain resolved, all targeted suites are green locally, and vp check is clean on the changed source. No blocking findings.

Verified resolved

  • #1 (build-time I/O gating)analyzeAppPrefetchCapabilities is now gated: toLinkPrefetchRoute skips it when prefetchVaryEnabled === false (app-browser-entry.ts:126), and both call sites in index.ts (:1522, :3828) compute the flag from cacheComponents && varyParams && optimisticRouting. Default path is I/O-free. ✅
  • #2 (footer overflow) — Fail-closed candidate ladder in encodeFooter/encodePrefetchVaryHeader (rsc-completion-metadata.ts:124-215). Header and footer degrade in lockstep through the same conservative encoders (app-page-render.ts:897-913), so overflow over-varies (varyAllParams/pageAllSuspenseDynamic) rather than reusing across params that vary. ✅
  • #3 (composite key ambiguity)encodeAppPrefetchRuntimeTemplateVariantKey uses JSON.stringify([a, b]) (app-route-prefetch-policy.ts:261-266). ✅

New observations this round

A. Cache-busting back-compat (outside the feature gate) — verified correct. app-rsc-cache-busting.ts adds VINEXT_PREFETCH_VARY_REQUEST_HEADER to the current hash input (and to VINEXT_RSC_VARY_HEADER). This is the one change that touches the non-gated request path for all users. The rollout is handled correctly: previous* hashes strip only the prefetch-vary header, and the new older* hashes strip both prefetch-vary and render-mode, so in-flight requests from an older client during a deploy still match an accepted hash. tests/app-rsc-cache-busting.test.ts (38 tests) covers this and passes.

B. Producer invariant holds by construction. getPrefetchVaryMetadata builds pageDynamicSuspenseOrdinals as the union of every element's ordinals (app-layout-param-observation.ts), so the defensive isOrdinalRecordCoveredByGlobalUnion validator never spuriously downgrades legitimate producer metadata — it only guards tampered wire data. Confirmed.

Minor / non-blocking

  • Latent trap: the sync Suspense-replacement branch in createOptimisticRouteElements is effectively dead but silently diverges from the probe/async ordinal counting. The probe (app-page-probe.ts:215) and the async replay (replaceObservedDynamicSuspenseChildrenAsync, which resolves react.lazy nodes via readReactLazyNode) both count Suspense ordinals through lazy subtrees. The sync replaceObservedDynamicSuspenseChildren (app-optimistic-routing.ts:598-604) does not resolve lazy nodes, so it would miscount ordinals for any serialized tree containing a lazy node followed by a Suspense boundary. Today this is unreachable: every preservePageElements template is always run through the async prepareOptimisticRouteTemplate before being stored (app-browser-entry.ts:516-518), which sets pageElementsPrepared = true, and createOptimisticRouteElements then continues past the sync branch (:593). So it's not a live bug — but it's a correctness landmine for a future refactor that ever stores an unprepared preserve-template. Consider either deleting the sync suspense-replacement branch (rely on the async prepare) or adding an assert(pageElementsPrepared) there so the invariant can't silently regress.
  • optimisticRouteTemplates keyed by variantKey can accumulate entries for a route hit with many distinct observed param values; cleared on prefetch invalidation, no per-route cap. Matches existing prefetch-cache behavior (unchanged from prior rounds).
  • Dev/prod parity holds — App-Router-only; prod delegates to the built RSC entry.

Verification

  • vp test run: rsc-completion-metadata (22), app-optimistic-routing (20), app-prefetch-vary-analysis (12), app-layout-param-observation (6), app-page-probe (40), app-page-element-builder (65), app-rsc-cache-busting (38), thenable-params (22), link, next-config (216), entry-templates (54) — all green.
  • vp check clean on all changed source files.

Verdict

The feature is fully gated behind the experimental triple-gate (cacheComponents && varyParams && optimisticRouting) and defaults off. All prior findings are resolved. The remaining item is a non-blocking maintainability note about dead-but-divergent sync code. No blocking findings for merge.

@ask-bonk

ask-bonk Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2861.

Summary

I performed an independent full re-review of all 40 changed files at head 93dbe24. This is round 4; the prior three rounds resolved the substantive findings. My review confirms no blocking findings.

Verified resolved:

New this round:

  • Cache-busting back-compat (the only non-gated change): verified correct — the new VINEXT_PREFETCH_VARY_REQUEST_HEADER is added to the current hash, and the previous*/older* hash variants strip it (and render-mode) so in-flight requests from older clients during a deploy still match. Covered by 38 passing tests.
  • Producer invariant: pageDynamicSuspenseOrdinals is the union of every element's ordinals by construction, so the defensive validator never downgrades legitimate metadata.

One non-blocking maintainability note: the sync Suspense-replacement branch in createOptimisticRouteElements (app-optimistic-routing.ts) is effectively dead but silently diverges from the probe/async ordinal counting — it doesn't resolve react.lazy nodes, so it would miscount ordinals if ever reached. It's unreachable today (every preserve-template is prepared via the async path first), but I recommended deleting it or adding an assert(pageElementsPrepared) so the invariant can't silently regress in a future refactor.

All targeted test suites (rsc-completion-metadata, app-optimistic-routing, app-prefetch-vary-analysis, app-page-probe, app-layout-param-observation, thenable-params, entry-templates, and more) pass locally, and vp check is clean. The feature stays fully gated behind the experimental triple-gate and defaults off.

github run

# Conflicts:
#	packages/vinext/src/client/vinext-next-data.ts
#	packages/vinext/src/entries/app-browser-entry.ts
#	packages/vinext/src/shims/internal/app-route-prefetch-policy.ts
#	packages/vinext/src/shims/link.tsx
#	packages/vinext/src/shims/navigation.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.

1 participant