Skip to content

fix(app-router): schedule cache component link prefetches - #2857

Open
james-elicx wants to merge 5 commits into
mainfrom
codex/fix-segment-prefetch-scheduling
Open

fix(app-router): schedule cache component link prefetches#2857
james-elicx wants to merge 5 commits into
mainfrom
codex/fix-segment-prefetch-scheduling

Conversation

@james-elicx

Copy link
Copy Markdown
Member

Summary

  • add a Cache Components Link prefetch scheduler with route-tree and segment phases
  • prioritize intent work, preserve batch ordering, and handle cancellation/re-entry
  • support automatic, explicit full, and unstable_dynamicOnHover strategies
  • omit the scheduler and global listeners when Cache Components is disabled

Next.js parity

Targets test/e2e/app-dir/segment-cache/prefetch-scheduling/prefetch-scheduling.test.ts.

Targeted Next.js E2E passed twice: 4 passed, 1 skipped.

REPO="$(pwd)" NEXTJS_DIR="/Users/jamesanderson/Developer/vinext/.nextjs-ref" ./scripts/run-targeted-nextjs-e2e.sh test/e2e/app-dir/segment-cache/prefetch-scheduling/prefetch-scheduling.test.ts

Validation

  • vp test run tests/link-segment-prefetch-bundle.test.ts tests/link-navigation.test.ts tests/link-segment-prefetch-scheduler.test.ts tests/prefetch-cache.test.ts tests/link.test.ts — 285 passed
  • vp run check
  • independent review — no findings

Known unrelated parity gap

The upstream unstable_dynamicOnHover case still rejects a duplicated static layout. Current main fails earlier because it does not fetch the dynamic content; this patch fixes the scheduling failure but does not broaden into duplicated-layout cache identity.

@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@2857
npm i https://pkg.pr.new/create-vinext-app@2857
npm i https://pkg.pr.new/@vinext/types@2857
npm i https://pkg.pr.new/vinext@2857

commit: cc5b2c9

@james-elicx

Copy link
Copy Markdown
Member Author

Run 31290819291 backlog mapping for test/e2e/app-dir/segment-cache/prefetch-scheduling/prefetch-scheduling.test.ts:

  • increases the priority of a viewport-initiated prefetch on hover
  • reserve special bandwidth for the most recently hovered link
  • cancels a viewport-initiated prefetch if the link leaves the viewport before it finishes
  • reschedules a link's prefetch when it re-enters the viewport

The run artifact recorded all four as failures (0 passed, 4 failed, 1 skipped). On this exact PR head, the targeted Next.js E2E passed twice with 4 passed, 1 skipped.

@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

@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 — fix(app-router): schedule cache component link prefetches

Reviewed the scheduler (link-segment-prefetch-scheduler.ts), the link.tsx integration, and the three test files. Ran the targeted suites and vp check on the changed source — all green (9 passed). No blocking correctness issues found; the design mirrors Next.js's Segment Cache scheduler faithfully and dev/prod parity is preserved via the build-time define. A few observations below, none blocking.

Verified correct

  • Cache Components gate & tree-shaking. CACHE_COMPONENTS_ENABLED checks both === "true" (dev/test string env) and === true (production boolean define from index.ts:2361, which JSON-stringifies nextConfig.cacheComponents ?? false). Consistent with the existing slot.tsx/app-page-route-wiring.tsx pattern, and link-segment-prefetch-bundle.test.ts confirms the scheduler and global listeners dead-strip when disabled.
  • No double-run on cancel+reschedule of a running task. schedule skips the phase reset while isRunning and enqueue early-returns for running tasks, so the in-flight promise's finishTaskPhase remains the single driver. The re-entry test covers this.
  • activeRequests accounting. Canceling a running task defers the decrement to finishTaskPhase, avoiding a double-decrement — correct.
  • Batch ordering. hasBandwidth's older-batch route-tree gate works because there is always ≥1 queued older-batch task while any remain unstarted; once older tasks advance to the segment phase they stop blocking newer route trees, which is acceptable.

Non-blocking observations

  1. previousIntentTask is only cleared when it finishes its segment phase while still visible (finishTaskPhase, lines 186–188, guarded by !isCanceled && instance.isVisible). If a previousIntentTask becomes invisible without going through cancel(), the reference lingers with isRunning === false. This is currently harmless because the reserved-bandwidth block in hasBandwidth additionally requires previousIntentTask.isRunning === true, and setVisibleLinkPrefetch/unmount both route invisibility through cancel(). Worth a short comment noting the invariant so a future refactor of the visibility path doesn't reintroduce a stale-reference block.

  2. if (prefetchMode === "disabled") return; (link.tsx:1380) is likely redundantshouldViewportPrefetch already gates via canLinkPrefetch (which accounts for isDangerous and prefetch === false, the only two ways resolveLinkPrefetchMode returns "disabled"). It is safe and also satisfies the narrowed fetchStrategy type, so keeping it is fine; just flagging it as belt-and-suspenders.

  3. forceSegmentCacheFetch is captured at route-tree schedule time and carried into the segment phase rather than re-evaluated against hasRecentUserInteraction() when the segment phase starts. The scheduler test at line 230 asserts this carry-over is intended, so this is by design — noting it only for future reference against Next.js parity.

Known gap (already disclosed)

The PR body correctly notes the upstream unstable_dynamicOnHover duplicated-static-layout case still rejects; this patch fixes the scheduling failure without broadening into duplicated-layout cache identity. Agreed that is out of scope here.

LGTM pending CI.

@ask-bonk

ask-bonk Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2857.

Summary

I reviewed the scheduler module, the link.tsx integration, and all three test files, then ran the targeted suites (tests/link-segment-prefetch-scheduler.test.ts, tests/link-segment-prefetch-bundle.test.ts — 9 passed) plus vp check on the changed source (clean).

No blocking correctness issues. The implementation faithfully mirrors Next.js's Segment Cache scheduler and preserves dev/prod parity through the build-time __NEXT_CACHE_COMPONENTS define.

Key things I verified:

  • The dual === "true" || === true gate correctly handles both the dev/test string env and the production boolean define (matching slot.tsx), and tree-shaking strips the scheduler when disabled.
  • No double-run or activeRequests miscount on the cancel + reschedule path for running tasks.
  • Batch-ordering and reserved-bandwidth gates behave as the tests assert.

Three non-blocking observations flagged in the review:

  1. previousIntentTask is only cleared when finishing its segment phase while visible — harmless today (the block also requires isRunning, and invisibility routes through cancel()), but worth a comment to protect the invariant.
  2. The new prefetchMode === "disabled" guard is likely redundant with shouldViewportPrefetch/canLinkPrefetch, though safe and type-narrowing.
  3. forceSegmentCacheFetch is captured at route-tree time and carried into the segment phase — confirmed intentional by the scheduler test.

The disclosed unstable_dynamicOnHover duplicated-static-layout gap is correctly scoped out.

github run

@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Performance benchmarks

Compared cc5b2c9 against base d558d62 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 135.2 KB 135.5 KB ⚫ +0.2%
Client entry size (gzip) vinext 122.7 KB 122.8 KB ⚫ +0.1%
Dev server cold start vinext 2.94 s 2.94 s ⚫ -0.2%
Production build time vinext 3.05 s 3.06 s ⚫ +0.3%
RSC entry closure size (gzip) vinext 114.2 KB 114.2 KB ⚫ -0.0%
Server bundle size (gzip) vinext 193.0 KB 193.1 KB ⚫ +0.0%

View detailed results and traces

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

@james-elicx

Copy link
Copy Markdown
Member Author

CI follow-up on exact head 018ba80cc2a2ad79e327720436074ef07a42848c:

  • reproduced the Chromium and WebKit segment-cache-metadata failures locally
  • narrowed recent-interaction forced segment fetches to automatic viewport prefetches
  • preserved the legacy headerless key for non-inlined explicit full prefetches so equivalent rewrite targets reuse cached content
  • removed the two unused exported scheduler types reported by Knip

Post-fix validation:

  • Chromium metadata parity: 2/2 passed
  • WebKit metadata parity: 2/2 passed
  • exact Next.js prefetch-scheduling E2E: 4 passed, 1 skipped
  • focused vinext tests: 285/285 passed
  • vp run check: clean

@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 — fix(app-router): schedule cache component link prefetches

Independent pass over the scheduler (link-segment-prefetch-scheduler.ts), the link.tsx integration, and the three test files. Ran the targeted suites and vp check on the changed source — all green (link-segment-prefetch-scheduler + link-segment-prefetch-bundle: 9 passed; link + link-navigation + prefetch-cache: 276 passed; vp check: clean). No blocking correctness issues.

Verified correct

  • Cache Components gate & tree-shaking. CACHE_COMPONENTS_ENABLED (link.tsx:977) checks both === "true" (dev/test string env) and === true. The production define at index.ts:2361 is JSON.stringify(nextConfig.cacheComponents ?? false), i.e. a real boolean, so the === true branch is load-bearing, not redundant. link-segment-prefetch-bundle.test.ts confirms the scheduler and global listeners dead-strip when disabled.
  • activeRequests accounting. A queued (never-started) task canceled via cancel() never touched activeRequests; a running task defers its decrement to finishTaskPhase (line 191), so cancel + reschedule of an in-flight task cannot double-decrement. drain removes a task from the queue before the canceled/invisible skip (lines 213–217), so findNextTask can't loop on the same task.
  • Cancel + reschedule of a running route-tree task. schedule skips the phase reset while isRunning (line 277) and enqueue early-returns for running tasks (line 169), so the original in-flight promise's finishTaskPhase stays the single driver that advances to the segment phase. Covered by the re-entry test.
  • Batch ordering. hasBandwidth's older-batch route-tree gate (lines 144–155) only blocks route-tree default tasks, and older tasks have smaller sortId so they'd otherwise lose the compareTasks tiebreak — the gate is what lets the older batch drain first. Once older tasks advance to segment they stop blocking newer route trees. No starvation since a blocked newer task always has ≥1 queued older task making forward progress.
  • forceSegmentCacheFetch carry-over. Captured at route-tree schedule time (line 261) and threaded through the segment phase; the scheduler test at line 230 asserts this carry-over and the prefetchUrl guard at link.tsx:668–671 consumes it to bypass an alias cache hit. Intentional.

Non-blocking observations

  1. previousIntentTask stale-reference invariant. It's only cleared on segment completion while visible (finishTaskPhase lines 193, 200–202). If a previousIntentTask ever became invisible without going through cancel(), the reference would linger. Harmless today because (a) hasBandwidth's reserved-bandwidth block additionally requires previousIntentTask.isRunning === true (line 135), and (b) every invisibility path — setVisibleLinkPrefetch and the effect-cleanup unmount in link.tsx — routes through cancel(), which clears it (line 296). Worth a one-line comment documenting the invariant so a future refactor of the visibility path doesn't reintroduce a stale block.

  2. cancel(mostRecentIntentTask) doesn't re-promote previousIntentTask. When the most-recent hover leaves, mostRecentIntentTask is nulled but the demoted previousIntentTask is not restored to intent. This is acceptable (the second-most-recent still keeps its reserved-bandwidth block while running, matching the "keep the earlier hover's response ahead" intent) — noting only for future parity comparison.

  3. if (prefetchMode === "disabled") return; (link.tsx:1383) is belt-and-suspenders. shouldViewportPrefetch already gates through canLinkPrefetch (which accounts for the only two ways resolveLinkPrefetchMode returns "disabled": isDangerous and prefetch === false). It's safe and also narrows the fetchStrategy type, so keeping it is fine.

Known gap (already disclosed)

The PR body correctly scopes out the upstream unstable_dynamicOnHover duplicated-static-layout case; this patch fixes the scheduling failure without broadening into duplicated-layout cache identity. Agreed that's out of scope here.

LGTM pending CI.

@ask-bonk

ask-bonk Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2857.

Summary

I independently reviewed the scheduler (link-segment-prefetch-scheduler.ts), the link.tsx integration, and the three test files, then ran the targeted suites (link-segment-prefetch-scheduler + link-segment-prefetch-bundle: 9 passed; link + link-navigation + prefetch-cache: 276 passed) plus vp check on the changed source (clean).

No blocking correctness issues. The scheduler faithfully mirrors Next.js's Segment Cache lifecycle and dev/prod parity is preserved via the build-time __NEXT_CACHE_COMPONENTS define.

Key things I verified independently:

  • activeRequests cannot double-decrement or leak across cancel + reschedule of a running task (decrement deferred to finishTaskPhase; queued cancels never incremented).
  • drain can't loop on a canceled/invisible task — it's removed from the queue before the skip.
  • Batch-ordering gate has no starvation — the older-batch block only applies to route-tree tasks, and a blocked newer task always has a queued older task making progress.
  • The dual === "true" || === true gate is load-bearing, not redundant: the production define at index.ts:2361 compiles to a real boolean, while the string branch covers dev/test.
  • Tree-shaking strips the scheduler and global listeners when disabled (bundle test).

Three non-blocking observations flagged (matching the prior review's read): the previousIntentTask stale-reference invariant (worth a comment), the non-re-promotion of previousIntentTask on cancel (acceptable parity choice), and the belt-and-suspenders prefetchMode === "disabled" guard. The disclosed unstable_dynamicOnHover duplicated-static-layout gap is correctly scoped out. LGTM pending CI.

github run

@james-elicx
james-elicx marked this pull request as ready for review August 10, 2026 08:38
@james-elicx
james-elicx marked this pull request as draft August 10, 2026 10:07
@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 — fix(app-router): schedule cache component link prefetches

Independent pass over the new scheduler (link-segment-prefetch-scheduler.ts), the link.tsx integration, and the navigation/popstate rewiring (navigation.ts, app-browser-entry.ts, navigation-runtime.ts). Ran the targeted suites and they're green:

  • link-segment-prefetch-scheduler + link-segment-prefetch-bundle: 14 passed
  • link + link-navigation: 211 passed
  • prefetch-cache: 71 passed

No blocking correctness issues. The scheduler mirrors Next.js's Segment Cache lifecycle, and the Cache-Components gate keeps the scheduler + global listeners fully dead-stripped when disabled.

Verified correct

  • activeRequests accounting is balanced. Each phase increments in drain (line 258) and decrements exactly once in finishTaskPhase (line 211) via .finally. A queued task canceled before it starts never touched the counter; a running task defers its decrement to finishTaskPhase, so cancel + reschedule of an in-flight task cannot double-decrement or leak.
  • No double-run on hover of a running task. With an unchanged strategy, schedule hits isRunning (line 332) and the inner if is false, so no restartAfterRunning is set — the original in-flight promise's finishTaskPhase stays the single driver and simply carries the now-intent priority into the segment phase. Matches the "increases priority on hover" Next.js case.
  • No drain self-loop. Canceled/invisible tasks are removed from the queue before the skip (lines 247-251), so findNextTask can't reselect them.
  • Batch-ordering gate can't deadlock/starve. The older-batch block only applies to route-tree default tasks and requires a queued older task; once older tasks run or advance to segment they stop blocking, and a blocked newer task always has a queued older task making progress.
  • Popstate cancellation parity is now cleaner. Moving the cancel from navigation.ts into the app-browser-entry.ts popstate handler (after the isSameAppRoutePopstateTarget early return, line 2464) means hash-only traversals no longer over-cancel prefetches — an improvement over the previous "over-cancel by destination" behavior the old comment documented.
  • The synchronous click-time cancel (link.tsx:1608-1616) guards on hasAppNavigationRuntime (the local boolean shadow at line 1552, not the imported fn) and skips hash-only changes — correct, and complements the runtime-wide cancelLinkPrefetchTasks hook for the split-chunk race it documents.
  • The dual === "true" || === true gate is load-bearing, not redundant: the production define compiles to a real boolean while the string branch covers dev/test, consistent with the existing slot.tsx pattern.

Non-blocking observations (agree with prior reviews)

  1. previousIntentTask stale-reference invariant. Cleared only on segment completion while visible (lines 231-233); harmless today because hasBandwidth's reserved-bandwidth block also requires previousIntentTask.isRunning === true and every invisibility path routes through cancel(). A one-line comment on the invariant would protect it against a future visibility-path refactor.
  2. if (prefetchMode === "disabled") return; (link.tsx:1398) is belt-and-suspenders relative to shouldViewportPrefetch/canLinkPrefetch, but it's safe and also narrows the fetchStrategy type — fine to keep.
  3. forceSegmentCacheFetch carry-over from route-tree schedule time into the segment phase is asserted intentional by the scheduler test.

Known gap (already disclosed)

The PR body correctly scopes out the upstream unstable_dynamicOnHover duplicated-static-layout case. Agreed that's out of scope here.

LGTM pending CI.

@ask-bonk

ask-bonk Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2857.

Summary

I independently reviewed the new scheduler (link-segment-prefetch-scheduler.ts), the link.tsx integration, and the navigation/popstate rewiring (navigation.ts, app-browser-entry.ts, navigation-runtime.ts), then ran the targeted suites — all green (14 + 211 + 71 passed).

No blocking correctness issues. The scheduler faithfully mirrors Next.js's Segment Cache lifecycle, and Cache Components dead-strips the scheduler and global listeners when disabled.

Key things I verified independently:

  • activeRequests is balanced — each phase increments once in drain and decrements once in finishTaskPhase via .finally; cancel + reschedule of an in-flight task can't double-decrement or leak.
  • No double-run when hovering a running task with an unchanged strategy; the in-flight promise stays the single driver and carries the boosted intent priority into the segment phase.
  • drain can't self-loop — canceled/invisible tasks are removed from the queue before the skip.
  • Batch-ordering gate can't deadlock or starve — only blocks route-tree default tasks and requires a queued older task making progress.
  • Popstate parity improved — moving the cancel into the browser-entry popstate handler after the same-route early return eliminates the old hash-only over-cancellation.

Three non-blocking observations (matching the prior reviews): the previousIntentTask stale-reference invariant (worth a comment), the belt-and-suspenders prefetchMode === "disabled" guard, and the intentional forceSegmentCacheFetch carry-over. The disclosed unstable_dynamicOnHover duplicated-static-layout gap is correctly scoped out.

LGTM pending CI.

github run

@james-elicx
james-elicx marked this pull request as ready for review August 10, 2026 11:26
…fetch-scheduling

# Conflicts:
#	packages/vinext/src/shims/link.tsx
@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 — fix(app-router): schedule cache component link prefetches

Independent pass over the new scheduler (link-segment-prefetch-scheduler.ts), the link.tsx two-phase prefetch integration, the prefetch-policy skipSegmentCacheScheduler opt-out, and the popstate/traversal cancellation rewiring (navigation.ts, app-browser-entry.ts, navigation-runtime.ts). Ran the targeted suites locally — all green:

  • link-segment-prefetch-scheduler + link-segment-prefetch-bundle: 14 passed
  • link-navigation: 85 passed

No blocking correctness issues. The scheduler mirrors Next.js's Segment Cache lifecycle, and the Cache-Components gate dead-strips the scheduler + global listeners when disabled.

Verified correct

  • Two-phase completion is real, not fire-and-forget. prefetchUrl now returns the inner async promise, and it awaits scheduledEntry?.pending (link.tsx:839-841) plus the learning-only fresh-entry pending (link.tsx:584-588) before resolving. So the scheduler's finishTaskPhase fires only after the route-tree / segment request actually settles — this is what makes the route-tree→segment handoff and the reserved-bandwidth ordering correct.
  • activeRequests is balanced. Each phase increments once in drain (line 258) and decrements exactly once in finishTaskPhase (line 211) via .finally. A queued task canceled before it starts never touched the counter; a running task defers its decrement to finishTaskPhase, so cancel + reschedule of an in-flight task can't double-decrement or leak. The restartAfterRunning + cancel interleaving also can't leak unfinishedTasks (cancel deletes it; finishTaskPhase line 235 re-deletes harmlessly).
  • No drain self-loop. Canceled/invisible tasks are removed from the queue before the skip (lines 247-251), so findNextTask can't reselect the same task.
  • Batch-ordering gate can't deadlock/starve. The older-batch block only applies to route-tree default tasks and requires a queued older task; a running/segment-phase older task stops blocking, and a blocked newer task always has a queued older task making progress.
  • previousIntentTask stale-reference is actually safe today. Every invisibility path routes through cancel()setVisibleLinkPrefetch(instance, false) (link.tsx:1038) and the effect-cleanup unmount which calls cancel(instance) (link.tsx:1434) before setting isVisible = false (link.tsx:1437) — and cancel clears both mostRecentIntentTask and previousIntentTask (scheduler lines 367-368). Combined with hasBandwidth's extra previousIntentTask.isRunning === true requirement, no stale block can form. Agree with prior reviews that a one-line invariant comment would protect this against a future visibility-path refactor, but it's non-blocking.
  • Click-time synchronous cancel (link.tsx:1614-1621) correctly guards on the local hasAppNavigationRuntime boolean (link.tsx:1558, not the imported fn) and skips hash-only changes via isHashOnlyBrowserUrlChange. The added navigation-epoch re-check after URL hashing (link.tsx:574) closes the click-wins-during-await gap; if the runtime-wide cancelLinkPrefetchTasks bumps the epoch, an in-flight segment prefetchUrl returns early and the already-canceled task's finishTaskPhase is a no-op.
  • Popstate parity improved. Moving the cancel out of notifyAppPopstateNavigationStart and into the browser-entry popstate handler after the isSameAppRoutePopstateTarget early return (app-browser-entry.ts:2464) eliminates the old hash-only over-cancellation the removed comment documented.
  • skipSegmentCacheScheduler opt-out correctly keeps encoded-slash / fully-dynamic-root learning-only paths on the single-shell request path (usesSegmentCachePrefetchScheduler returns false), avoiding a duplicate segment request for those cache-identity-sensitive routes.
  • Dual === "true" || === true gate is load-bearing, not redundant — the production define compiles to a real boolean while the string branch covers dev/test, consistent with slot.tsx.

Non-blocking observations (agree with prior reviews)

  1. previousIntentTask invariant comment (above) — worth adding.
  2. if (prefetchMode === "disabled") return; (link.tsx:1406) is belt-and-suspenders vs shouldViewportPrefetch/canLinkPrefetch, but it's safe and narrows the fetchStrategy type — fine to keep.
  3. forceSegmentCacheFetch carry-over from route-tree schedule time into the segment phase is asserted intentional by the scheduler test.

Known gap (already disclosed)

The PR body correctly scopes out the upstream unstable_dynamicOnHover duplicated-static-layout case; this patch fixes the scheduling failure without broadening into duplicated-layout cache identity. Agreed that's out of scope.

LGTM pending CI.

@ask-bonk

ask-bonk Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2857.

Summary

I independently reviewed the new scheduler (link-segment-prefetch-scheduler.ts), the link.tsx two-phase prefetch integration, the skipSegmentCacheScheduler opt-out (app-route-prefetch-policy.ts), and the popstate/traversal cancellation rewiring (navigation.ts, app-browser-entry.ts, navigation-runtime.ts). Ran the targeted suites locally: scheduler + bundle (14 passed) and link-navigation (85 passed).

No blocking correctness issues. The implementation faithfully mirrors Next.js's Segment Cache scheduler and preserves dev/prod parity via the build-time __NEXT_CACHE_COMPONENTS define.

Key things I verified independently (beyond prior reviews):

  • The two-phase completion is real, not fire-and-forget. prefetchUrl now returns its inner async promise and awaits scheduledEntry?.pending (and the learning-only fresh-entry pending) before resolving, so the scheduler's finishTaskPhase fires only after the actual request settles. This is the load-bearing mechanism behind the route-tree→segment handoff and reserved-bandwidth ordering.
  • activeRequests is balanced across cancel + reschedule of in-flight tasks; the restartAfterRunning + cancel interleaving can't leak unfinishedTasks.
  • The navigation-epoch re-check after URL hashing (link.tsx:574) closes the click-wins-during-await gap and integrates correctly with the synchronous click-time cancel (which uses the local hasAppNavigationRuntime boolean, not the imported fn).
  • previousIntentTask stale-reference is safe today — every invisibility path routes through cancel() (including the unmount cleanup, which cancels before setting isVisible = false), so no stale reserved-bandwidth block can form.
  • Popstate cancellation parity improved by moving the cancel after the same-route early return.
  • skipSegmentCacheScheduler correctly keeps encoded-slash / fully-dynamic-root routes on the single-shell path to avoid duplicate segment requests.

Three non-blocking observations flagged (matching prior reviews): the previousIntentTask invariant comment, the belt-and-suspenders prefetchMode === "disabled" guard, and the intentional forceSegmentCacheFetch carry-over. The disclosed unstable_dynamicOnHover duplicated-static-layout gap is correctly scoped out.

LGTM pending CI.

github run

# Conflicts:
#	packages/vinext/src/shims/internal/app-route-prefetch-policy.ts
#	packages/vinext/src/shims/link.tsx
#	tests/link-navigation.test.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant