Skip to content

fix(app-router): start optimistic prefetches immediately#2440

Merged
james-elicx merged 9 commits into
mainfrom
codex/fix-optimistic-routing-prefetch
Jul 1, 2026
Merged

fix(app-router): start optimistic prefetches immediately#2440
james-elicx merged 9 commits into
mainfrom
codex/fix-optimistic-routing-prefetch

Conversation

@james-elicx

Copy link
Copy Markdown
Member

Summary

  • start App Router viewport prefetches immediately instead of waiting for browser idle
  • keep low-priority Pages fallback document prefetches on requestIdleCallback
  • add a focused regression for visible App Router links dispatching before idle callbacks

Next.js parity

Fixes the optimistic-routing failures from deploy-suite run 28478866791:

  • test/e2e/app-dir/optimistic-routing/optimistic-routing.test.ts

Validation

  • vp check packages/vinext/src/shims/link.tsx tests/link-navigation.test.ts
  • vp test run tests/link-navigation.test.ts tests/app-optimistic-routing.test.ts --maxWorkers=1 — 67 passed
  • NEXT_TEST_CONCURRENCY=1 REPO="$(pwd)" NEXTJS_DIR="/Users/jamesanderson/Developer/vinext/.nextjs-ref" ./scripts/run-targeted-nextjs-e2e.sh test/e2e/app-dir/optimistic-routing/optimistic-routing.test.ts — 9 passed

Notes

The broader app-prefetch protocol cluster from the same run appears covered by #2318, so this PR stays focused on the optimistic-routing dispatch timing issue.

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@pkg-pr-new

pkg-pr-new Bot commented Jun 30, 2026

Copy link
Copy Markdown

Open in StackBlitz

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

commit: 245fd22

@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: start optimistic prefetches immediately

I reviewed the diff against main, traced the call sites, and ran the targeted tests. Verdict: the change is correct and well-scoped. Tests pass locally (tests/link-navigation.test.ts 55✓, tests/app-optimistic-routing.test.ts 12✓) and tsc is clean for link.tsx.

What the change does (and why it's right)

The old code gated all low-priority prefetches behind requestIdleCallback. Viewport-triggered App Router prefetches are dispatched with "low" priority (setVisibleLinkPrefetchprefetchUrl(..., "low", ...) at packages/vinext/src/shims/link.tsx:712 and pingVisibleLinkPrefetches at :727), so they were being deferred to browser idle. That's the exact behavior the Next.js optimistic-routing e2e asserts against. Moving the App Router path to run synchronously is the correct fix.

The split is clean:

  • priority === "high" → immediate (unchanged behavior; high always ran immediately).
  • hasAppNavigationRuntime() → immediate (new; covers low-priority viewport prefetches).
  • Pure Pages Router, low priority → still requestIdleCallback / setTimeout fallback.

Test quality

The new regression is genuinely meaningful, not a tautology. The default renderIsolatedLink helper stubs requestIdleCallback as synchronous (tests/link-navigation.test.ts:1187), so the pre-existing "prefetches visible links" test would pass with or without this fix. The new test correctly overrides requestIdleCallback with a non-invoking vi.fn(() => 1) and asserts requestIdleCallback is never called, which actually proves the bypass. Good.

Minor: stale doc comment (non-blocking)

The prefetchUrl JSDoc still claims idle scheduling is always used:

packages/vinext/src/shims/link.tsx:398
 * Uses `requestIdleCallback` (or `setTimeout` fallback) to avoid blocking
 * the main thread during initial page load.

After this change that's only true for pure-Pages low-priority prefetches. Worth a one-line update so the comment matches the new scheduling logic, e.g. note that App Router and high-priority prefetches dispatch immediately and only low-priority Pages prefetches defer to idle.

Things I checked and confirmed are fine

  • Hybrid apps: when App runtime is present, the inner hybrid-ownership guard (:477-480) returns early for Pages/document-owned URLs rather than falling through to the Pages document-prefetch else if, so the "keep Pages document prefetches on idle" claim holds — those don't run through this path at all when App runtime exists.
  • High-priority Pages intent prefetch: still runs immediately, matching prior behavior; the existing intent-prefetch test (:1751) remains consistent.

LGTM aside from the doc comment nit.

@ask-bonk

ask-bonk Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2440.

Summary

I reviewed PR #2440 (fix(app-router): start optimistic prefetches immediately) and posted a review. The change is correct and well-scoped.

What I verified:

  • The diff correctly moves App Router (and high-priority) prefetches to run immediately, while keeping pure-Pages low-priority prefetches on requestIdleCallback. Viewport prefetches are dispatched as "low" priority (link.tsx:712, :727), so they were previously being deferred to idle — this is the exact behavior the Next.js optimistic-routing e2e tests fail on.
  • The new regression test is genuinely meaningful (not a tautology): the default test helper stubs requestIdleCallback synchronously, so the pre-existing test wouldn't catch this; the new test overrides it with a non-invoking mock and asserts idle is never called.
  • Ran both referenced test files locally: link-navigation.test.ts (55✓), app-optimistic-routing.test.ts (12✓). tsc clean for the changed file.
  • Confirmed hybrid-app and high-priority Pages paths behave consistently.

One non-blocking nit raised: the prefetchUrl JSDoc (link.tsx:398) still claims idle scheduling is always used, which is now stale after this change.

No correctness or parity issues found.

github run

@james-elicx james-elicx force-pushed the codex/fix-optimistic-routing-prefetch branch from 3181bf0 to 96d2dec Compare June 30, 2026 23:00
@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: start optimistic prefetches immediately

Independently re-reviewed against main: traced the call sites, checked dev/prod and hybrid-app parity, and ran the targeted tests. The change is correct and well-scoped. No correctness or parity issues.

Verified

  • Root cause matches the fix. Viewport prefetches are dispatched as "low" priority (setVisibleLinkPrefetchprefetchUrl(..., "low", ...) and pingVisibleLinkPrefetches), so the old priority === "high" ? immediate : requestIdleCallback gate deferred all App Router viewport prefetches to browser idle. The new gate if (priority === "high" || hasAppNavigationRuntime()) runs them immediately, which is what the Next.js optimistic-routing e2e asserts.

  • Pages parity is preserved, including hybrid apps. The "keep low-priority Pages fallback document prefetches on idle" claim holds:

    • Pure Pages app (hasAppNavigationRuntime() false): low-priority prefetches still go through requestIdleCallback / setTimeout fallback (link.tsx:654). Unchanged.
    • Hybrid app (App runtime present): a Pages/document-owned URL now runs immediately, enters the hasAppNavigationRuntime() block, and returns early at the hybrid-owner guard (link.tsx:478-481) — it never reaches the else if document-prefetch branch. This was already true on main (the else if (HAS_PAGES_ROUTER ...) branch is unreachable whenever the App runtime exists), so this PR only changes timing on the App path, not which branch runs.
  • Test is a genuine regression guard, not a tautology. The default renderIsolatedLink helper stubs requestIdleCallback to fire synchronously (link-navigation.test.ts:1187), so the pre-existing "prefetches visible links" test would pass with or without the fix. The new test overrides it with a non-invoking vi.fn(() => 1) and asserts requestIdleCallback is never called — that actually proves the bypass.

  • Local runs: tests/link-navigation.test.ts 55✓, tests/app-optimistic-routing.test.ts 12✓. tsc clean for link.tsx.

Non-blocking nit (already addressed)

The earlier review's point about the stale prefetchUrl JSDoc has been fixed in this revision (link.tsx:398-400 now correctly distinguishes App/high-priority immediate dispatch from low-priority Pages idle scheduling).

LGTM.

@ask-bonk

ask-bonk Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2440.

Summary

I independently re-reviewed PR #2440 (fix(app-router): start optimistic prefetches immediately). The change is correct and well-scoped — no correctness or parity issues found.

What I verified:

  • Root cause/fix match. Viewport prefetches dispatch as "low" priority, so the old priority === "high" ? immediate : requestIdleCallback gate deferred all App Router viewport prefetches to browser idle. The new if (priority === "high" || hasAppNavigationRuntime()) gate runs them immediately — exactly what the Next.js optimistic-routing e2e asserts.
  • Pages parity holds, including hybrid apps. Pure Pages apps still defer low-priority prefetches to idle (link.tsx:654). In hybrid apps, Pages/document-owned URLs return early at the hybrid-owner guard (link.tsx:478-481) and never hit the else if document-prefetch branch — and that was already true on main, so this PR only changes timing on the App path.
  • Test is a genuine regression guard. The default helper stubs requestIdleCallback synchronously (which would mask the bug); the new test overrides it with a non-invoking mock and asserts it's never called.
  • Local runs: link-navigation.test.ts 55✓, app-optimistic-routing.test.ts 12✓, tsc clean.

Note: The stale JSDoc nit raised in the earlier review has already been fixed in this revision (link.tsx:398-400).

Verdict: LGTM.

github run

@james-elicx james-elicx marked this pull request as ready for review June 30, 2026 23:45
@james-elicx

Copy link
Copy Markdown
Member Author

This looks like it covers the run 28478866791 / job 84413308650 test/e2e/app-dir/optimistic-routing/optimistic-routing.test.ts failure cluster. I am skipping a duplicate backlog fix for those optimistic-routing failures.

@james-elicx

Copy link
Copy Markdown
Member Author

Deploy-suite run 28478866791 / job 84413308650 backlog check: this PR appears to cover the test/e2e/app-dir/segment-cache/prefetch-auto/prefetch-auto.test.ts failure. I reproduced the current failure as Timed out waiting for a request to be initiated when revealing <Link prefetch="auto">; this PR starts App Router viewport prefetches immediately, which is the exact missing request-initiation path. Skipping a duplicate implementation pass for this item.

@james-elicx

Copy link
Copy Markdown
Member Author

Deploy-suite run 28478866791 / job 84413308650 backlog check: this PR appears to cover the test/e2e/app-dir/segment-cache/force-stale/force-stale.test.ts failure, specifically force stale > during a navigation, don’t request segments that have a pending "full" prefetch already in progress. I reproduced the current failure as Timed out waiting for a request to be initiated when revealing the prefetch={true} dynamic link; this PR starts App Router viewport prefetches immediately, which is the missing request-initiation path. Skipping a duplicate implementation pass for this item.

@james-elicx

Copy link
Copy Markdown
Member Author

Verified #2440 also covers test/e2e/app-dir/segment-cache/prefetch-runtime/prefetch-runtime.test.ts: the baseline branch reproduces Timed out waiting for a request to be initiated for runtime prefetching can completely prefetch a page that is fully static, and the targeted upstream file passes on #2440. This backlog item should be considered covered by #2440; no duplicate PR needed.

@james-elicx

Copy link
Copy Markdown
Member Author

Backlog duplicate check for run 28478866791 / job 84413308650: this PR likely covers the remaining non-cacheComponents/use-cache optimistic-routing dynamic and catch-all route prediction failures. The fix starts App Router viewport prefetches immediately, and the PR validation reports the exact upstream target test/e2e/app-dir/optimistic-routing/optimistic-routing.test.ts passing 9/9, so I am skipping a duplicate implementation pass.

@james-elicx james-elicx enabled auto-merge (squash) July 1, 2026 16:42
@james-elicx james-elicx merged commit 03f0217 into main Jul 1, 2026
63 checks passed
@james-elicx james-elicx deleted the codex/fix-optimistic-routing-prefetch branch July 1, 2026 16:45
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