Skip to content

refactor(navigation): route prefetch reuse through planner#1962

Merged
james-elicx merged 5 commits into
cloudflare:mainfrom
NathanDrake2406:nathan/pr4-prefetch-reuse
Jun 13, 2026
Merged

refactor(navigation): route prefetch reuse through planner#1962
james-elicx merged 5 commits into
cloudflare:mainfrom
NathanDrake2406:nathan/pr4-prefetch-reuse

Conversation

@NathanDrake2406

Copy link
Copy Markdown
Contributor

Overview

Field Details
Goal Implement PR 4 from #1790 by moving prefetch reuse selection into the navigation planner.
Core change The browser executor now supplies typed visited-response, prefetch, and optimistic-shell candidate facts to navigationPlanner.
Boundary Planner decides reuse/fetch intent. Executor still owns cache maps, awaiting prefetches, fetches, React rendering, and history effects.
Primary files navigation-planner.ts, app-browser-entry.ts, navigation.ts, planner/cache tests.
Expected impact No intended user-visible behavior change. This makes prefetch reuse policy testable without browser or React execution.

Why

Navigation cache reuse is a control-plane decision: it depends on navigation kind, cache-bypass policy, candidate availability, and whether an optimistic shell can be attempted. The browser executor should collect those facts and perform effects, but it should not own the semantic ordering of visited response reuse, prefetch consumption, optimistic shell attempts, and fresh fetch fallback.

Area Principle / invariant What this PR changes
Visited response cache Freshness, refresh, and mounted-slot compatibility decide whether a cached response may be reused. Adds planner classification for visited-response candidates while leaving LRU and eviction effects in the cache helper.
Prefetch consumption A navigation may share an eligible prefetch candidate, including an in-flight request, before issuing a fresh fetch. Adds a planner reuse decision and keeps the actual await/consume path in the browser executor.
Optimistic shell Optimistic route shell rendering is a detached fallback only after authoritative cache/prefetch reuse is unavailable. Planner decides when the executor may attempt optimistic shell learning and rendering.

What changed

Scenario Before After
Fresh visited response Browser entry chose the branch inline. Planner returns reuseVisitedResponse.
Eligible prefetch Browser entry checked navigation kind/cache bypass inline before consuming. Planner returns consumePrefetch from typed candidate facts.
Failed or stale prefetch probe Navigation fell through to optimistic shell/fetch. Same behavior, with a second planner decision after failed consumption.
Stale navigation probe Expired entries were removed silently through the consume path. Candidate probing can opt out of invalidation notifications to preserve that behavior.
Optimistic shell Browser entry decided when to learn/render optimistic templates. Planner returns attemptOptimisticRouteShell; executor still performs learning/rendering.
Maintainer review path
  1. packages/vinext/src/server/navigation-planner.ts: review the new candidate fact and reuse decision types, plus the decision ordering in classifyNavigationReuse.
  2. packages/vinext/src/server/app-browser-entry.ts: review how the executor collects candidate facts and how failed prefetch consumption is replanned before optimistic/fresh fetch fallback.
  3. packages/vinext/src/shims/navigation.ts: review the backward-compatible notifyInvalidation option used only by navigation candidate probes.
  4. tests/navigation-planner-prefetch-reuse.test.ts and tests/prefetch-cache.test.ts: review the planner decision table and stale-probe notification regression coverage.
Validation
  • vp check
  • vp test run tests/navigation-planner-prefetch-reuse.test.ts tests/navigation-planner.test.ts tests/navigation-planner-rsc-fetch-result.test.ts tests/navigation-planner-early-intent.test.ts tests/prefetch-cache.test.ts tests/app-visited-response-cache.test.ts tests/app-optimistic-routing.test.ts
  • pnpm knip
  • pnpm run test:e2e tests/e2e/app-router/nextjs-compat/prefetch.spec.ts
Risk / compatibility
  • Public API: adds an optional notifyInvalidation option to the existing internal prefetch cache probe. Existing callers keep the previous default behavior.
  • Runtime: no intended user-visible behavior change. The PR preserves refresh cache bypass, same-page search cache bypass, in-flight prefetch reuse, and silent stale-prefetch eviction for navigation probes.
  • Architecture: this is intentionally a control-plane slice. It does not introduce segment cache storage or OperationToken authority.

References

Reference Why it matters
#1790 Tracks PR 4 scope: visited-response cache lookup, prefetch consumption, and optimistic route shell learning.
Next.js segment-cache prefetch tests Informs the prefetch reuse expectation and the in-flight duplicate request regression shape.

Navigation cache reuse decisions were still split across the browser executor, which made visited-response reuse, prefetch consumption, and optimistic shell attempts hard to verify at the control-plane boundary.

Move the reuse decision table into navigationPlanner with typed candidate facts, keep cache reads and React effects in the executor, and preserve silent stale-prefetch eviction for navigation probes.
@pkg-pr-new

pkg-pr-new Bot commented Jun 12, 2026

Copy link
Copy Markdown

Open in StackBlitz

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

commit: 818fb43

Visited response cache misses were represented in the planner API, but the browser executor still returned before invoking that classifier when the entry was absent or intentionally bypassed.

That made the missing-candidate branch planner-owned in tests but executor-local in production. The executor now builds candidate facts for both present and missing states, asks the planner for the decision, and keeps cache mutation in the executor as the effect boundary.
@james-elicx

Copy link
Copy Markdown
Member

/bigbonk review for correctness 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.

Reviewed for correctness. The planner extraction is clean and the decision-ordering tests are thorough. One behavior change stands out that contradicts the stated "no intended user-visible behavior change" / "silent stale-prefetch eviction for navigation probes preserved" goal: the prefetch probe now runs on the visited-response-hit path, where the old code never touched the prefetch cache.

Details inline.

Comment thread packages/vinext/src/server/app-browser-entry.ts
@ask-bonk

ask-bonk Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Review posted successfully.

Summary

I reviewed PR #1962 (route prefetch reuse through planner) for correctness and found one behavioral regression:

Unconditional prefetch probe on the visited-response-hit path (app-browser-entry.ts:1787-1795)

hasPrefetchCandidate is now computed unconditionally, including when cachedRoute is non-null. In the prior code, the prefetch cache was only touched after the if (cachedRoute) { ... return/continue } branch — so on a visited-response cache hit, the prefetch cache was never probed or mutated for that navigation.

After this change, hasPrefetchCacheEntryForNavigation(..., { notifyInvalidation: false }) runs before the reuse decision. Since classifyNavigationReuse prefers reuseVisitedResponse, the prefetch is never consumed — but the probe has already silently evicted any stale/expired prefetch entry that the old code would have left in place, plus it adds wasted work on the documented cache-hit soft-nav fast path.

This contradicts the PR's stated "no intended user-visible behavior change" and "silent stale-prefetch eviction for navigation probes preserved" claims, and it isn't covered by the new tests (they exercise the planner in isolation, not the executor's unconditional probe). I posted a suggestion to gate the probe on cachedRoute === null.

The rest of the change (planner decision types, ordering, trace codes, and the notifyInvalidation option) is correct and well-tested.

github run

Visited-response reuse should stay on the soft-navigation fast path without touching the prefetch cache. The planner extraction accidentally computed prefetch availability before deciding to reuse a visited response, so stale prefetch entries could be silently evicted on a path that previously left them untouched.

Add a planner prefetch-probe decision and have the browser executor consult it before reading the prefetch cache. This keeps cache mutation in the executor while making the no-probe condition explicit and tested.
@james-elicx

Copy link
Copy Markdown
Member

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

Reviewed for correctness. The planner extraction is clean, the decision ordering in classifyNavigationReuse matches the prior inline branching, and the new classifyNavigationPrefetchProbe gate (commit 92b083d) correctly resolves the earlier concern about probing the prefetch cache on a visited-response hit — on a visited hit visitedResponse.status === "available", so classifyNavigationPrefetchProbe returns skip/visitedResponseAvailable and the probe no longer runs. The notifyInvalidation: false option also preserves silent stale-prefetch eviction, and the in-flight (pending) prefetch reuse path is intact.

I did find one remaining behavioral divergence from the pre-refactor code on the cache-miss path: a slot-mismatched exact-key prefetch entry is no longer evicted. Details inline.

Comment thread packages/vinext/src/server/app-browser-entry.ts
@ask-bonk

ask-bonk Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Review posted successfully.

Summary

I reviewed PR #1962 (route prefetch reuse through planner) for correctness. The refactor is clean and well-tested. Key findings:

Resolved since the prior review: The earlier bonk concern (unconditional prefetch probe on the visited-response-hit path) was fixed by commit 92b083d, which adds classifyNavigationPrefetchProbe. On a visited-response hit, visitedResponse.status === "available", so the probe returns skip/visitedResponseAvailable and no longer touches the prefetch cache. The notifyInvalidation: false option correctly preserves silent stale-prefetch eviction, and in-flight (pending) prefetch reuse is intact.

One remaining divergence (posted inline at app-browser-entry.ts:1904): Slot-mismatched exact-key prefetch entries are no longer evicted on the cache-miss path. The old code ran consumePrefetchResponseForNavigation unconditionally for every non-refresh/non-bypass miss, which reaches the exact-key slot-mismatch eviction branch in consumePrefetchResponse (navigation.ts:962-968). The new code gates the consume on hasPrefetchCandidate, which is derived from findPrefetchCacheEntryForNavigation — a function that deliberately skips slot-incompatible exact entries. So that eviction branch is now unreachable, leaving slot-mismatched entries in the cache (eventually reclaimed by the FIFO/expiry sweep). It's not a hard correctness bug, but it's a real divergence from the "no intended user-visible behavior change" claim and isn't covered by the new tests. I suggested either preserving the eviction or documenting it as deliberate, plus a regression test.

Everything else — planner decision types, ordering, trace codes, and the notifyInvalidation option — is correct.

github run

@james-elicx

Copy link
Copy Markdown
Member

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

Reviewed for correctness. The planner extraction is clean, classifyNavigationReuse ordering faithfully mirrors the old inline branching (including non-refresh/non-bypass prefetch consumption for both navigate and traverse, and the navigate-only optimistic-shell gate), and the classifyNavigationPrefetchProbe gate (commit 92b083d) correctly resolves the earlier concern about probing the prefetch cache on a visited-response hit. The notifyInvalidation: false option preserves silent stale-prefetch eviction, and in-flight (pending) prefetch reuse is intact. Tests, lint, and types all pass locally.

Your reply to the previous review thread is correct: the old async consumePrefetchResponseForNavigation path bails at findPrefetchCacheEntryForNavigation returning null and never reached the sync consumePrefetchResponse slot-mismatch eviction branch. So that branch was not reachable on the executor's miss path before this PR.

The one thing worth surfacing: the fix you landed in 970440523 (evictIncompatibleExactEntry: true) doesn't restore prior behavior — it adds an eviction the old code never performed on the navigation miss path. That's defensible (it cleans an unreusable entry), but it's a real, undocumented behavior change and your own review reply concluded "No code change needed here." One inline note below; this is a documentation/intent nit, not a blocker.

Comment thread packages/vinext/src/server/app-browser-entry.ts Outdated
@ask-bonk

ask-bonk Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Review posted successfully.

Summary

I reviewed PR #1962 (route prefetch reuse through planner) for correctness against the current head (970440523, which is newer than what the two prior bonk reviews saw). The refactor is solid — clean planner extraction, faithful decision ordering, passing tests/lint/types.

Key findings:

  1. The earlier bonk review's slot-mismatch-eviction concern was incorrect, and the author's refutation is right. I traced it: the old executor miss path used the async consumePrefetchResponseForNavigation, which bails at findPrefetchCacheEntryForNavigation returning null for slot-mismatched exact-only entries — it never reached the sync consumePrefetchResponse eviction branch. So that branch was unreachable on the executor path before this PR.

  2. One real (but benign) new divergence: The author's final fix commit added evictIncompatibleExactEntry: true to the probe. This doesn't preserve prior behavior — it adds an eviction the old executor never performed, in the opposite direction from the earlier reviewer's claim. It's defensible (cleans an unreusable entry, condition matches the canonical sync path), but it contradicts both the PR's "no intended user-visible behavior change" claim and the author's own "No code change needed here" reply. I posted an inline note asking them to either drop the option (faithful preservation) or document it as deliberate, plus a minor asymmetry note about the hardcoded notifyInvalidation: false in the new eviction.

Everything else — planner types, ordering, trace codes, classifyNavigationPrefetchProbe gate, traverse handling, and the prefetch fallback replanning — is correct. This is a documentation/intent nit, not a blocker.

github run

@james-elicx

Copy link
Copy Markdown
Member

/bigbonk review for issues

@ask-bonk

ask-bonk Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

@james-elicx Bonk workflow was cancelled.

View workflow run · To retry, trigger Bonk again.

@james-elicx

Copy link
Copy Markdown
Member

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

Reviewed the current head (818fb43) for correctness. The planner extraction is clean and faithfully preserves prior executor behavior. Both prior bonk concerns are now resolved at this head:

  1. Unconditional prefetch probe on the visited-hit path (first review): fixed by classifyNavigationPrefetchProbe (92b083d). On a visited hit visitedResponse.status === "available", so the probe returns skip/visitedResponseAvailable and hasPrefetchCacheEntryForNavigation is never called. Verified.

  2. evictIncompatibleExactEntry adding a new eviction (third review): the option that landed in 970440523 was removed in the final commit 818fb43 (fix(navigation): keep slot mismatch probe behavior). git show 970440523:.../navigation.ts has it; HEAD does not. The only navigation.ts change now is the notifyInvalidation option, so the slot-mismatch path is byte-identical to main. The author's earlier refutation (the async consumePrefetchResponseForNavigation wrapper bails at findPrefetchCacheEntryForNavigationnull and never reaches the sync slot-mismatch eviction branch) is correct.

I independently re-verified the behavioral parity that matters:

  • Expired non-pending entry on a miss: old path deleted it with notify=false via consumePrefetchResponse (line 983); new probe deletes it with notify=false via the notifyInvalidation: false option (navigation.ts:639). Equivalent — and locked down by the new prefetch-cache.test.ts test.
  • In-flight (pending) prefetch: probe returns true without awaiting (navigation.ts:631), then consumePrefetch awaits via the wrapper. Reuse intact.
  • Bypass / slot-mismatch visited cache: synthetic candidate: "missing" and the evict decision branches reproduce the old getVisitedResponse delete-on-read semantics exactly.
  • traverse vs navigate: classifyNavigationReuse consumes prefetch for both kinds and gates attemptOptimisticRouteShell to navigate-only, matching the old inline branching.

vp check and the targeted tests (navigation-planner-prefetch-reuse, prefetch-cache) pass locally.

Two non-blocking nits below.

Comment thread packages/vinext/src/server/app-browser-entry.ts
Comment thread packages/vinext/src/server/app-browser-entry.ts
@ask-bonk

ask-bonk Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Review posted successfully.

Summary

I reviewed PR #1962 at the current head (818fb43) for correctness. The refactor is clean and faithfully preserves prior executor behavior. Both prior bonk concerns are now resolved at this head:

  1. Unconditional prefetch probe on the visited-hit path (1st review) — fixed by classifyNavigationPrefetchProbe in 92b083d. On a visited hit, the probe returns skip/visitedResponseAvailable and never touches the prefetch cache.

  2. evictIncompatibleExactEntry adding a new eviction (3rd review) — that option (added in 970440523) was removed in the final commit 818fb43. I confirmed via git show that it's gone at HEAD; the only navigation.ts change now is the notifyInvalidation option, so the slot-mismatch path is byte-identical to main. The author's earlier refutation was correct.

I independently re-verified the behavioral parity points that matter:

  • Expired non-pending entry: old and new both delete with notify=false (locked down by the new test).
  • In-flight (pending) prefetch reuse: intact (probe returns true without awaiting, consume awaits).
  • Bypass / slot-mismatch visited cache: synthetic missing + evict decisions reproduce old delete-on-read semantics.
  • traverse vs navigate gating in classifyNavigationReuse matches the old inline branching.

vp check and the targeted tests pass locally.

I posted two non-blocking nits inline: (1) the planner .trace fields are computed but never consumed in the executor (dead allocation on the soft-nav path), and (2) routeManifest is now computed eagerly even on the visited-hit path — harmless since it's a trivial property read, but flagged so it isn't mistaken for a regression against the deferral comment. No correctness blockers.

github run

@james-elicx james-elicx merged commit b5619c1 into cloudflare:main Jun 13, 2026
44 checks passed
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