refactor(navigation): route prefetch reuse through planner#1962
Conversation
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.
commit: |
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.
|
/bigbonk review for correctness issues |
There was a problem hiding this comment.
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.
|
Review posted successfully. SummaryI reviewed PR #1962 (route prefetch reuse through planner) for correctness and found one behavioral regression: Unconditional prefetch probe on the visited-response-hit path (
After this change, 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 The rest of the change (planner decision types, ordering, trace codes, and the |
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.
|
/bigbonk review for issues |
There was a problem hiding this comment.
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.
|
Review posted successfully. SummaryI 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 One remaining divergence (posted inline at Everything else — planner decision types, ordering, trace codes, and the |
|
/bigbonk review for issues |
There was a problem hiding this comment.
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.
|
Review posted successfully. SummaryI reviewed PR #1962 (route prefetch reuse through planner) for correctness against the current head ( Key findings:
Everything else — planner types, ordering, trace codes, |
|
/bigbonk review for issues |
|
@james-elicx Bonk workflow was cancelled. View workflow run · To retry, trigger Bonk again. |
|
/bigbonk review for issues |
There was a problem hiding this comment.
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:
-
Unconditional prefetch probe on the visited-hit path (first review): fixed by
classifyNavigationPrefetchProbe(92b083d). On a visited hitvisitedResponse.status === "available", so the probe returnsskip/visitedResponseAvailableandhasPrefetchCacheEntryForNavigationis never called. Verified. -
evictIncompatibleExactEntryadding a new eviction (third review): the option that landed in970440523was removed in the final commit818fb43(fix(navigation): keep slot mismatch probe behavior).git show 970440523:.../navigation.tshas it; HEAD does not. The onlynavigation.tschange now is thenotifyInvalidationoption, so the slot-mismatch path is byte-identical tomain. The author's earlier refutation (the asyncconsumePrefetchResponseForNavigationwrapper bails atfindPrefetchCacheEntryForNavigation→nulland 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=falseviaconsumePrefetchResponse(line 983); new probe deletes it withnotify=falsevia thenotifyInvalidation: falseoption (navigation.ts:639). Equivalent — and locked down by the newprefetch-cache.test.tstest. - In-flight (pending) prefetch: probe returns
truewithout awaiting (navigation.ts:631), thenconsumePrefetchawaits via the wrapper. Reuse intact. - Bypass / slot-mismatch visited cache: synthetic
candidate: "missing"and theevictdecision branches reproduce the oldgetVisitedResponsedelete-on-read semantics exactly. - traverse vs navigate:
classifyNavigationReuseconsumes prefetch for both kinds and gatesattemptOptimisticRouteShellto 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.
|
Review posted successfully. SummaryI reviewed PR #1962 at the current head (
I independently re-verified the behavioral parity points that matter:
I posted two non-blocking nits inline: (1) the planner |
Overview
navigationPlanner.navigation-planner.ts,app-browser-entry.ts,navigation.ts, planner/cache tests.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.
What changed
reuseVisitedResponse.consumePrefetchfrom typed candidate facts.attemptOptimisticRouteShell; executor still performs learning/rendering.Maintainer review path
packages/vinext/src/server/navigation-planner.ts: review the new candidate fact and reuse decision types, plus the decision ordering inclassifyNavigationReuse.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.packages/vinext/src/shims/navigation.ts: review the backward-compatiblenotifyInvalidationoption used only by navigation candidate probes.tests/navigation-planner-prefetch-reuse.test.tsandtests/prefetch-cache.test.ts: review the planner decision table and stale-probe notification regression coverage.Validation
vp checkvp 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.tspnpm knippnpm run test:e2e tests/e2e/app-router/nextjs-compat/prefetch.spec.tsRisk / compatibility
notifyInvalidationoption to the existing internal prefetch cache probe. Existing callers keep the previous default behavior.OperationTokenauthority.References