Skip to content

fix(app-router): avoid runtime route-tree prefetch sizing#2467

Merged
james-elicx merged 17 commits into
mainfrom
codex/fix-segment-cache-max-prefetch-inlining-28478866791
Jul 1, 2026
Merged

fix(app-router): avoid runtime route-tree prefetch sizing#2467
james-elicx merged 17 commits into
mainfrom
codex/fix-segment-cache-max-prefetch-inlining-28478866791

Conversation

@james-elicx

Copy link
Copy Markdown
Member

Summary

  • avoid rendering user components during route-tree prefetch size checks
  • normalize object-form experimental.prefetchInlining thresholds, including Infinity, and pass resolved values into the App RSC handler
  • add route-tree prefetch sizing/config/codegen coverage plus a focused browser fixture

Stacked on #2440 (codex/fix-optimistic-routing-prefetch) to avoid bundling the prerequisite optimistic-prefetch diff.

Validation

  • vp check on touched source/test files
  • vp test run tests/app-route-tree-prefetch.test.ts tests/next-config.test.ts
  • vp test run tests/app-rsc-handler.test.ts -t "route-tree"
  • vp test run tests/link-navigation.test.ts -t "prefetchInlining|deduped"
  • vp test run tests/app-router-next-config-codegen.test.ts tests/app-route-tree-prefetch.test.ts tests/next-config.test.ts
  • vp run vinext#build
  • 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/max-prefetch-inlining/max-prefetch-inlining.test.ts

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@pkg-pr-new

pkg-pr-new Bot commented Jul 1, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@vinext/cloudflare@2467
npm i https://pkg.pr.new/vinext@2467

commit: d738f95

@github-actions

github-actions Bot commented Jul 1, 2026

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

@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.

Review: avoid runtime route-tree prefetch sizing

Solid change — moving route-tree prefetch sizing off a real RSC render is the right call for a hot prefetch path, and the test coverage (deterministic hint trees, config normalization, the userComponentTrapModule guard) is thorough. A few things worth addressing, mostly leftover plumbing from the pre-render approach.

1. Dead renderer parameter — _renderToReadableStream is now completely unused

createRouteTreePrefetchResponse still takes _renderToReadableStream: RouteTreePrefetchRenderer (app-route-tree-prefetch.ts:399) but nothing in the module references it anymore. The caller in app-rsc-handler.ts:1066 still threads options.renderToReadableStream through, and CreateAppRscHandlerOptions.renderToReadableStream remains a required field wired from the generated entry.

Before this PR (HEAD~2) the renderer was genuinely used to measure gzip sizes by rendering components. Now it's pure dead plumbing spanning the handler options, the generated entry, and this signature. Consider dropping the parameter entirely (and the option, if nothing else uses it) so the API stops advertising a capability it no longer exercises. If there's a near-term plan to reintroduce real sizing, a short comment saying so would be better than a silently-unused param.

2. Wasted per-request work: params resolved then discarded

estimatePrefetchSize ignores its params (_params, _searchParams at app-route-tree-prefetch.ts:186-187), yet buildTree still computes them for every node on every prefetch request:

  • collectSearchParams(options.searchParams) (:403)
  • resolveAppPageSegmentParams(...) per segment and slot owner (:227, :238, :255)
  • resolveAppPageBranchParams(...) per slot child (:270)

resolveAppPageSegmentParams/resolveAppPageBranchParams do O(segments) allocation + Set construction per node (app-page-params.ts:50, :77). All of it feeds a function that throws the values away. This is exactly the kind of hot-path work AGENTS.md flags ("Keep the common request path lightweight", "Avoid repeated hot-path work"). Since sizing is now heuristic and param-independent, the params/searchParams threading through buildTree/createNode/estimatePrefetchSize can be removed.

3. Dead branch: staticPrefetchDisabled can never be true

computePrefetchHints branches on staticPrefetchDisabled = (node.prefetchHints & STATIC_PREFETCH_DISABLED) !== 0 (:294), and several code paths depend on it (:308-309, :331, :344). But prefetchHints is always initialized to 0 in createNode (:119) and nothing sets HAS_RUNTIME_PREFETCH / PREFETCH_DISABLED / STATIC_PREFETCH_DISABLED anywhere in the codebase before computePrefetchHints runs (confirmed by grep across src/). So staticPrefetchDisabled is always false and those branches are unreachable.

This is a faithful port of Next.js's algorithm, but in Next.js those bits are populated from route config like dynamic = 'force-static'. In vinext they never are, which means routes with export const dynamic/runtime-prefetch semantics won't get the static-prefetch-disabled treatment — a real parity gap, not just dead code. Either:

  • wire the bits from route metadata so the branches actually fire, or
  • if that's intentionally out of scope, drop the unreachable branches and add a comment/issue documenting the gap so the next reader doesn't assume it works.

4. prefetchSize export is a vinext-only convention (not a Next.js API)

explicitPrefetchSize reads module.prefetchSize with "large"/"small"/number (:176-182), and the fixtures export export const prefetchSize = "large" to force outlining. Worth a brief comment noting this is a vinext test/escape-hatch convention (Next.js sizes by rendering), so it isn't mistaken for an upstream API that users can rely on. Given vinext can't measure gzip sizes without rendering, the heuristic tradeoff itself is reasonable — just document it.

Minor

  • normalizePrefetchInliningConfig (next-config.ts:1349) keeps negative maxSize/maxBundleSize verbatim (Number.isFinite(-5) is true). Next.js clamps only non-finite values to MAX_SAFE_INTEGER; a negative threshold here would silently disable all inlining. Low priority, but a >= 0 guard (mirroring staleTimes handling right above it) would be more robust.

None of these block correctness of the happy path (tests pass), but #1 and #3 leave misleading dead surface area and #2/#3 have real hot-path/parity implications.

@ask-bonk

ask-bonk Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2467. Summary of what I found:

Review Summary

The PR correctly moves route-tree prefetch sizing off a real RSC render — the right call for a hot path, with thorough test coverage. But the cleanup from the old render-based approach was left incomplete. Findings:

  1. Dead renderer parameter_renderToReadableStream is now entirely unused in app-route-tree-prefetch.ts, yet still threaded through the handler options and generated entry. Before this PR it was used to render components for gzip measurement; now it's pure dead plumbing.

  2. Wasted per-request workbuildTree still computes collectSearchParams, resolveAppPageSegmentParams, and resolveAppPageBranchParams (O(segments) allocations per node) only to feed estimatePrefetchSize, which ignores them (_params/_searchParams). This contradicts AGENTS.md's hot-path guidance.

  3. Unreachable branch + parity gapstaticPrefetchDisabled can never be true because prefetchHints is always 0 and nothing sets STATIC_PREFETCH_DISABLED/PREFETCH_DISABLED anywhere. In Next.js those bits come from route config (dynamic = 'force-static'), so this is a real parity gap, not just dead code.

  4. Undocumented convention — the prefetchSize module export ("large"/"small"/number) is vinext-only, not a Next.js API; worth a comment so it isn't mistaken for upstream behavior.

  5. MinornormalizePrefetchInliningConfig keeps negative thresholds verbatim; a >= 0 guard (like the sibling staleTimes handling) would be more robust.

Targeted tests (app-route-tree-prefetch.test.ts, next-config.test.ts) pass. None of these block the happy path, but #1 and #3 leave misleading dead surface, and #2/#3 have real performance/parity implications.

github run

@james-elicx james-elicx force-pushed the codex/fix-segment-cache-max-prefetch-inlining-28478866791 branch from 7c38460 to eb4dabc Compare July 1, 2026 06:32
@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.

Review: avoid runtime route-tree prefetch sizing

The rewrite in eb4dabc cleanly resolves the earlier review's concerns — the render-based sizing, the dead renderToReadableStream plumbing in the handler, and the discarded params/searchParams threading are all gone. The heuristic sizing approach is a reasonable tradeoff given vinext can't measure gzip sizes without rendering, and the userComponentTrapModule test is a nice guard. Two things to address, one blocking.

1. Blocking: type error breaks CI Check

tests/app-rsc-handler.test.ts still passes renderToReadableStream to createAppRscHandler, but the option was removed from CreateAppRscHandlerOptions in this same PR. vp check fails:

× typescript(TS2353): Object literal may only specify known properties, and
  'renderToReadableStream' does not exist in type 'CreateAppRscHandlerOptions<TestRoute>'.
   tests/app-rsc-handler.test.ts:140:5

× typescript(TS2339): Property 'renderToReadableStream' does not exist on type
  'CreateAppRscHandlerOptions<TestRoute>'.
   tests/app-rsc-handler.test.ts:140:70

The runtime tests pass (the extra property is ignored at runtime), so this is purely a type failure — but it will fail the required Check job. The fix is to drop the stale option (line 140) and the now-unused import { renderToReadableStream } from "react-dom/server.edge" (line 3). I've pushed that fix.

2. Parity: negative-threshold clamping diverges from Next.js

normalizePrefetchInliningConfig (next-config.ts:1349) clamps negative thresholds to Number.MAX_SAFE_INTEGER via the >= 0 guard, and tests/next-config.test.ts now asserts this. But Next.js only clamps non-finite values — it does not special-case negatives. From packages/next/src/server/config.ts (assignDefaultsAndValidate):

result.experimental.prefetchInlining = {
  // Clamp Infinity to a finite value so the config survives
  // JSON.stringify (used by output: standalone).
  maxSize: Number.isFinite(maxSize) ? maxSize : Number.MAX_SAFE_INTEGER,
  maxBundleSize: Number.isFinite(maxBundleSize) ? maxBundleSize : Number.MAX_SAFE_INTEGER,
}

A negative finite value (-1) stays -1 in Next.js, which effectively disables all inlining (nothing is < maxSize). vinext instead turns -1 into MAX_SAFE_INTEGER, which inlines everything — the opposite behavior. Per AGENTS.md, divergences from Next.js should be deliberate and documented. Since the sole purpose of the clamp is JSON-survivability of Infinity, matching Next.js (Number.isFinite(x) ? x : MAX_SAFE_INTEGER) and dropping the >= 0 guard would restore parity. If keeping the guard is intentional (defense against a footgun), a comment noting the deliberate divergence would help. The earlier suggestion to add >= 0 was actually against upstream behavior.

Everything else looks good: Infinity/partial-object/default resolution matches Next.js, the codegen embedding is correct, dev/prod parity is preserved (App Router prod server delegates to the RSC entry, and route-tree prefetch is App-Router-only so Pages Router is unaffected), and the link.tsx two-stage /_tree/__PAGE__ gate is well-tested.

Co-authored-by: james-elicx <james-elicx@users.noreply.github.com>
@ask-bonk

ask-bonk Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2467. I also fixed the blocking type error in the test file (this is included in the response and will be pushed automatically).

Summary

The rewrite in eb4dabc cleanly resolved the earlier review's concerns (dead renderer plumbing, discarded params). Two findings:

  1. Blocking — type error breaks CI Check. tests/app-rsc-handler.test.ts:140 still passed renderToReadableStream to createAppRscHandler, but this PR removed that option from CreateAppRscHandlerOptions. vp check fails with TS2353 + TS2339. Runtime tests pass (extra prop ignored), so it's purely a type failure that would still fail the required Check job. Fixed by removing the stale option and its now-unused react-dom/server.edge import; verified vp check and all 117 handler tests pass.

  2. Parity — negative-threshold clamping diverges from Next.js. normalizePrefetchInliningConfig clamps negative maxSize/maxBundleSize to MAX_SAFE_INTEGER via a >= 0 guard, and the new test asserts this. But Next.js (config.ts assignDefaultsAndValidate) only clamps non-finite values (Number.isFinite(x) ? x : MAX_SAFE_INTEGER), so a negative stays negative and disables inlining — the opposite of vinext, which would inline everything. Flagged as a deliberate-vs-accidental divergence for the author to decide (the earlier >= 0 suggestion was actually against upstream).

Everything else (Infinity/partial/default resolution, codegen embedding, dev/prod parity, the link.tsx /_tree/__PAGE__ two-stage gate) checks out.

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.

Review: avoid runtime route-tree prefetch sizing

Re-reviewed at a8f32b6. The two prior review rounds are fully resolved:

  • Type error (was blocking): tests/app-rsc-handler.test.ts no longer passes the removed renderToReadableStream option and the unused react-dom/server.edge import is gone. vp check on all touched files now passes (format + lint + types).
  • Negative-threshold parity: a8f32b6 drops the >= 0 guard, so normalizePrefetchInliningConfig now matches Next.js's assignDefaultsAndValidate exactly (Number.isFinite(x) ? x : MAX_SAFE_INTEGER). Verified against packages/next/src/server/config.ts on canary — a negative finite value stays negative (disabling inlining) rather than being coerced to MAX_SAFE_INTEGER (inlining everything). The test in next-config.test.ts was updated to assert the parity behavior. Good.
  • Dead renderer plumbing / discarded params / unreachable StaticPrefetchDisabled branches: all gone in the eb4dabc rewrite. The bit constants (ParentInlinedIntoSelf = 0b100000, InlinedIntoChild = 0b1000000, HeadInlinedIntoSelf = 0b10000000) match packages/next/src/shared/lib/app-router-types.ts.

Verification run locally:

  • vp check tests/app-rsc-handler.test.ts tests/next-config.test.ts packages/vinext/src/server/app-route-tree-prefetch.ts packages/vinext/src/config/next-config.ts — pass
  • vp test run tests/app-route-tree-prefetch.test.ts tests/next-config.test.ts — 221 passed
  • vp test run tests/app-router-next-config-codegen.test.ts tests/app-rsc-handler.test.ts — 145 passed

Two non-blocking observations, both documentation-only:

1. HeadOutlined is never set on the root (untested edge case)

computePrefetchHints sets HeadInlinedIntoSelf when a page accepts the head inline, but never sets the root's HeadOutlined bit (0b100000000) in the fallback case where the head can't be inlined (inlinedBytes + HEAD_INLINE_SIZE >= maxBundleSize). Next.js sets HeadOutlined on the root so the client scheduler knows to fetch the head separately. Every fixture case ends with (+metadata) on a __PAGE__, so this path is never exercised — and with HEAD_INLINE_SIZE = 1 it only triggers under a tiny maxBundleSize. Vinext's own client also doesn't read these bits (it uses the two-stage /_tree/__PAGE__ gate in link.tsx), so the practical impact is limited to the raw Next.js client scheduler compatibility surface. A one-line comment noting HeadOutlined is intentionally not emitted (or a fixture that forces head outlining) would close the loop.

2. Number(value) coercion diverges slightly from Next.js for non-number thresholds

normalizePrefetchInliningConfig calls Number(maxSize) before the finite check, whereas Next.js checks Number.isFinite(maxSize) on the raw value. For a stringified numeric threshold (maxSize: "512"), vinext resolves 512 while Next.js would fall through to MAX_SAFE_INTEGER. This only matters for malformed configs the schema would normally reject, and vinext is the more lenient of the two — noting for completeness, not asking for a change.

Everything else (Infinity/partial/default resolution, codegen JSON.stringify embedding with a covering test, dev/prod parity via the RSC-entry delegation, the userComponentTrapModule guard, dynamic-param serialization, parallel-slot ordering) checks out. The heuristic sizing tradeoff (size 1 for any real component, prefetchSize escape hatch for forced outlining) is reasonable given vinext can't measure gzip sizes without rendering, and it's now documented in explicitPrefetchSize.

@ask-bonk

ask-bonk Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2467.

Summary

I re-reviewed at the current head (a8f32b6). Both prior review rounds are fully resolved and the PR is in good shape:

Resolved:

  1. Blocking type errortests/app-rsc-handler.test.ts no longer passes the removed renderToReadableStream option; vp check on all touched files passes.
  2. Negative-threshold paritya8f32b6 drops the >= 0 guard so normalizePrefetchInliningConfig now exactly matches Next.js's config.ts (Number.isFinite(x) ? x : MAX_SAFE_INTEGER), verified against canary source.
  3. Dead renderer plumbing, discarded params, unreachable StaticPrefetchDisabled branches — all removed in the eb4dabc rewrite. Bit constants match app-router-types.ts.

Verified locally:

  • vp check on the 4 touched source/test files — pass
  • tests/app-route-tree-prefetch.test.ts + tests/next-config.test.ts — 221 passed
  • tests/app-router-next-config-codegen.test.ts + tests/app-rsc-handler.test.ts — 145 passed

Two non-blocking, documentation-only observations:

  1. HeadOutlined (root bit) is never emitted when the head can't inline — an untested edge case only reachable under a tiny maxBundleSize; vinext's own client doesn't read these bits, so impact is limited to raw Next.js client compatibility.
  2. Number(value) coercion is slightly more lenient than Next.js for stringified thresholds (malformed-config edge case only).

No blocking issues. The heuristic sizing tradeoff is reasonable and now documented.

github run

@james-elicx james-elicx marked this pull request as ready for review July 1, 2026 06:48

@NathanDrake2406 NathanDrake2406 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.

Review: route-tree prefetch inlining

Requesting changes on one contract issue, plus one structural cleanup I think should happen before this pattern spreads.

1. Blocking: route-tree dynamic segment siblings use the wrong data model

packages/vinext/src/server/app-route-tree-prefetch.ts:211 passes route.staticSiblings into every dynamic route-tree segment. That field is route-wide and flattened across all dynamic URL levels; the existing route graph docs say so explicitly. But TreePrefetchParam.siblings is segment-local, matching Next's dynamic segment tuple.

For a route like /[locale]/products/[id], any static sibling discovered at one dynamic level can be attached to the other dynamic segment in the route-tree response. That gives the client incorrect segment-cache metadata and makes the new helper depend on a field that was only shaped for the older full RSC payload.

Can we either emit segment-local static siblings into the route manifest and thread that into the route-tree builder, or set route-tree siblings to null until that data exists? Reusing the flattened field here is the wrong ownership boundary.

2. Structural: route-tree prefetches are being modeled as optimistic loading shells

packages/vinext/src/shims/link.tsx:543 reuses the loading-shell gate path for route-tree prefetches and stores the /_tree response with optimisticRouteShell: true. That leaks a new transport concept into unrelated optimistic-template learning: app-browser-entry.ts scans settled prefetch entries and tries to decode them as App element payloads.

It mostly avoids hard failure because the learning loop awaits Promise.allSettled, but the shape is misleading. A route-tree response is not a loading-shell payload and should not be tagged as one. I think this wants an explicit cache kind, e.g. prefetchKind: "route-tree" | "loading-shell" | "navigation", or a dedicated route-tree pending gate outside the navigation prefetch cache.

CI is green, and the earlier renderer-plumbing/type/parity issues are resolved. This review is about keeping the route-tree contract and cache model clean before more segment-cache behavior builds on top of it.

…he-max-prefetch-inlining-28478866791

# Conflicts:
#	packages/vinext/src/shims/link.tsx
…he-max-prefetch-inlining-28478866791

# Conflicts:
#	tests/app-rsc-handler.test.ts
@james-elicx

Copy link
Copy Markdown
Member Author

Deploy-suite run 28478866791 / report job 84413308650 triage: this PR appears to cover the App Router max prefetch inlining rows from test/e2e/app-dir/segment-cache/max-prefetch-inlining/max-prefetch-inlining.test.ts.

Exact rows:

  • max prefetch inlining > bundles all segment data into a single request per route
  • max prefetch inlining > deduplicates inlined prefetch requests for the same route

Since this PR is already James-owned, I am skipping duplicate implementation work for these rows.

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Performance benchmarks

Compared d738f95 against base 96d2dec using alternating same-runner rounds. Next.js was unchanged and skipped.

0 improved · 0 regressed · 6 within ±1.5%

Scenario Framework Baseline Current Change
Client bundle size (gzip) vinext 124.2 KB 125.0 KB ⚫ +0.6%
Client entry size (gzip) vinext 118.8 KB 119.4 KB ⚫ +0.5%
Dev server cold start vinext 2.54 s 2.53 s ⚫ -0.4%
Production build time vinext 3.12 s 3.16 s ⚫ +1.5%
RSC entry closure size (gzip) vinext 95.6 KB 96.8 KB ⚫ +1.2%
Server bundle size (gzip) vinext 161.9 KB 163.2 KB ⚫ +0.8%

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 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.

Review: avoid runtime route-tree prefetch sizing

Re-reviewed at d738f95. The latest commit ("keep route-tree prefetch metadata scoped") resolves both blocking issues from the prior review round, and all earlier concerns (dead renderer plumbing, discarded params, type error, negative-threshold parity) remain resolved. No blocking issues.

Prior blockers — both resolved

1. Dynamic segment siblings used the wrong data model — fixed.
staticSiblings is entirely gone from the route-tree path. AppRouteTreePrefetchRoute no longer declares it, createNode/routeTreeSegment/dynamicRouteTreeSegment no longer thread it, and dynamicRouteTreeSegment now emits siblings: null with a comment explaining that vinext only has the flattened full-RSC-payload list and leaves segment-local siblings unknown until segment-local route metadata exists (app-route-tree-prefetch.ts:139-155). The route-wide staticSiblings field is now only used by the legitimate full-RSC-payload path (app-page-route-wiring.tsx, app-rsc-manifest.ts) — the incorrect reuse in the route-tree builder is removed. The handler route type (app-rsc-handler.ts:118-126) is consistent — no stale staticSiblings forwarding. The test was updated to assert siblings: null for /[slug].

2. Route-tree prefetches modeled as optimistic loading shells — fixed.
There's now an explicit PrefetchCacheKind = "loading-shell" | "navigation" | "route-tree" on the cache entry (navigation.ts:256, mirrored in next-shims.d.ts). Route-tree prefetches are tagged prefetchKind: "route-tree" with optimisticRouteShell: false (link.tsx:596-597) instead of being mislabeled as a loading shell. The optimistic-template learning loop now skips route-tree entries before trying to decode them as App element payloads (app-browser-entry.ts:506), and promotePrefetchEntriesForNavigation skips them too (link.tsx:708). Navigation consumption already excluded them via cacheForNavigation === false (navigation.ts:460); the new tag makes the intent explicit and self-documenting. New tests cover it: prefetch-cache.test.ts ("keeps route-tree prefetch responses out of navigation consumption") and link-navigation.test.ts asserts the route-tree entry has cacheForNavigation: false / optimisticRouteShell: false.

Verified locally at d738f95

  • vp check on the 8 touched source/test files — pass (format + lint + types)
  • tests/app-route-tree-prefetch.test.ts + tests/next-config.test.ts — 221 passed
  • tests/prefetch-cache.test.ts + tests/link-navigation.test.ts — 95 passed
  • tests/app-rsc-handler.test.ts + tests/app-router-next-config-codegen.test.ts + tests/shims.test.ts — 1323 passed

Non-blocking (documentation-only, unchanged from prior round)

  1. HeadOutlined root bit never emitted. computePrefetchHints sets HeadInlinedIntoSelf but never sets the root's HeadOutlined bit in the fallback path (head can't inline). Vinext's own client doesn't read these hint bits — it uses the two-stage /_tree → segment gate in link.tsx — so impact is limited to raw Next.js client scheduler compatibility, and with HEAD_INLINE_SIZE = 1 it only triggers under a tiny maxBundleSize. A one-line comment (or a fixture forcing head outlining) would close the loop.
  2. Number(value) coercion is slightly more lenient than Next.js for stringified thresholds (malformed-config edge case only; vinext is the more permissive of the two).

The heuristic sizing tradeoff (size 1 for any real component, prefetchSize escape hatch for forced outlining) is documented in explicitPrefetchSize, dev/prod parity holds (route-tree prefetch is App-Router-only and the prod server delegates to the RSC entry), and the bit constants match app-router-types.ts. Good to merge once CI is green.

@ask-bonk

ask-bonk Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2467.

Summary

I re-reviewed at the current head (d738f95), which is the "keep route-tree prefetch metadata scoped" commit. Both blocking issues from NathanDrake2406's prior review are fully resolved, and all earlier rounds' concerns remain resolved. No blocking issues.

Resolved blockers:

  1. Wrong data model for dynamic segment siblingsstaticSiblings is entirely removed from the route-tree path. dynamicRouteTreeSegment now emits siblings: null with a comment explaining vinext only has the flattened full-RSC-payload list. The route-wide staticSiblings field is now only used by the legitimate full-RSC path. Test updated to assert siblings: null.

  2. Route-tree prefetches modeled as optimistic loading shells — an explicit PrefetchCacheKind ("loading-shell" | "navigation" | "route-tree") now tags entries. Route-tree prefetches are tagged "route-tree" with optimisticRouteShell: false, and the optimistic-template learning loop + navigation promotion both skip them. New tests in prefetch-cache.test.ts and link-navigation.test.ts guard the behavior.

Verified locally: vp check on all 8 touched files passes; targeted test suites total 221 + 95 + 1323 passing.

Two non-blocking, documentation-only observations carried over: the HeadOutlined root bit is never emitted (only affects raw Next.js client scheduler compat, not vinext's own two-stage gate), and Number(value) coercion is slightly more lenient than Next.js for malformed stringified thresholds.

github run

Base automatically changed from codex/fix-optimistic-routing-prefetch to main July 1, 2026 16:45
@james-elicx james-elicx merged commit ebf8e59 into main Jul 1, 2026
60 checks passed
@james-elicx james-elicx deleted the codex/fix-segment-cache-max-prefetch-inlining-28478866791 branch July 1, 2026 16:47
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.

2 participants