Skip to content

fix(app-router): skip prefetches for bots#2323

Merged
james-elicx merged 3 commits into
mainfrom
codex/fix-bot-prefetch-parity
Jun 26, 2026
Merged

fix(app-router): skip prefetches for bots#2323
james-elicx merged 3 commits into
mainfrom
codex/fix-bot-prefetch-parity

Conversation

@james-elicx

Copy link
Copy Markdown
Member

Summary

  • skip App Router Link viewport/intent prefetches for bot user agents
  • skip programmatic router.prefetch() for bots through the same existing Next.js-compatible matcher
  • add focused unit coverage plus a production browser regression

Parity target

Report 28143992598 showed test/e2e/app-dir/app-prefetch/prefetching.test.ts failing should not prefetch for a bot user agent because vinext issued an RSC request for /static-page.

Next.js v16.2.6 gates all prefetch URL creation with isBot(window.navigator.userAgent) in packages/next/src/client/components/app-router-utils.ts.

This track intentionally avoids:

Validation

  • vp check packages/vinext/src/shims/link.tsx packages/vinext/src/shims/navigation.ts tests/link-navigation.test.ts tests/prefetch-cache.test.ts tests/e2e/app-router/nextjs-compat/prefetch-bot.browser.spec.ts
  • vp test run tests/link-navigation.test.ts tests/prefetch-cache.test.ts --maxWorkers=1
  • PLAYWRIGHT_PROJECT=app-router-chrome-browser-specific npx playwright test tests/e2e/app-router/nextjs-compat/prefetch-bot.browser.spec.ts --workers=1 --retries=0
  • NEXTJS_PREPARE=0 NEXT_TEST_CONCURRENCY=1 vp env exec --node 24 ./scripts/run-nextjs-deploy-suite.sh /Users/jamesanderson/Developer/vinext/.nextjs-ref --retries 0 -c 1 --debug test/e2e/app-dir/app-prefetch/prefetching.test.ts
    • selected assertion should not prefetch for a bot user agent passes
    • the suite still reports the known unrelated legacy loading/protocol/priority failures tracked outside this PR

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@pkg-pr-new

pkg-pr-new Bot commented Jun 25, 2026

Copy link
Copy Markdown

Open in StackBlitz

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

commit: 7576a6f

@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

@github-actions

github-actions Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Performance benchmarks

Compared 7576a6f against base de014d4 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 117.1 KB 117.4 KB ⚫ +0.3%
Client entry size (gzip) vinext 112.5 KB 112.8 KB ⚫ +0.3%
Dev server cold start vinext 2.48 s 2.48 s ⚫ -0.0%
Production build time vinext 2.95 s 2.94 s ⚫ -0.3%
RSC entry closure size (gzip) vinext 93.0 KB 93.0 KB ⚫ +0.0%
Server bundle size (gzip) vinext 156.9 KB 157.2 KB ⚫ +0.1%

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 commented Jun 25, 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 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: skip prefetches for bots

Scoped, well-tested change. The two production gates (<Link> viewport/intent prefetch in link.tsx:438 and router.prefetch() in navigation.ts:1852) are placed correctly, the unit + browser coverage is solid, and vp check + the two unit files pass locally. The E2E spec is a genuine regression guard: it asserts no RSC: 1 prefetch on hover for a Googlebot UA but does still navigate (exactly one RSC request) on click, matching Next.js. The request.headers().rsc === "1" assertion is valid — createRscRequestHeaders sets RSC: "1" (app-rsc-cache-busting.ts:281) and Playwright lowercases header names.

Issue: next/form App Router prefetch is not gated for bots

The PR's stated goal is to "skip App Router Link viewport/intent prefetches" and router.prefetch() for bots, gating all App Router RSC prefetch creation. But there is a third App Router RSC prefetch entry point that issues the same kind of request and is left ungated:

packages/vinext/src/shims/form.tsx:311-369 — the <Form> viewport IntersectionObserver prefetches the action route via the same unified createRscRequestUrl + prefetchRscResponse path used by <Link> and router.prefetch(). There is no isBotUserAgent check anywhere in form.tsx (confirmed: no isBot/userAgent reference in the file).

Consequence: a bot scrolling a page containing an App Router <Form action="/..."> still triggers an RSC prefetch for the action route — the exact behavior this PR suppresses for <Link>. This is an inconsistency within vinext's own prefetch model, since all three call sites share the same RSC prefetch mechanism.

Note on Next.js parity: in Next.js v16.2.6 the App Router <Form> prefetch goes through the segment-cache scheduler (mountFormInstance), a different subsystem than createPrefetchURL/isBot — and this PR intentionally avoids the segment-cache track. So this is not strictly a Next.js-parity miss for Form. But because vinext routes Form prefetch through the same RSC path the PR is gating, leaving it ungated produces divergent bot behavior across vinext's own Link/router/Form prefetches.

Suggested fix — add the same guard inside the form viewport observer callback, mirroring link.tsx:438:

// form.tsx, inside the IntersectionObserver callback (before createRscRequestUrl)
if (isBotUserAgent(window.navigator?.userAgent ?? "")) return;

with import { isBotUserAgent } from "../utils/html-limited-bots.js";. A focused unit test (bot UA → form viewport intersection issues no fetch) would round out the coverage to match the new Link/router tests.

Minor notes (non-blocking)

  • Bot-list scope vs Next.js isBot. isBotUserAgent combines the HTML-limited bot list + the headless Googlebot regex. Next.js's client createPrefetchURL uses isBot() from shared/lib/router/utils/is-bot.ts. These are intended to be equivalent per the utility's doc comment; worth a quick confirmation that the combined list is a superset of (and doesn't diverge from) Next's isBot for the prefetch use case, since the same util is reused for the Pages fallback path.
  • Ordering in router.prefetch. assertSafeNavigationUrl(href) runs before the bot check, whereas Next's createPrefetchURL does the bot check first (then URL parse). The URL-parse-throws ordering relative to the bot check matches Next (bot check before parse), so this is fine; assertSafeNavigationUrl is vinext defense-in-depth and running it unconditionally is acceptable.

Overall the core change is correct and safe to ship; the next/form gap is the one item I'd want addressed (here or as an immediate follow-up) so bot suppression is consistent across all three App Router prefetch entry points.

@ask-bonk

ask-bonk Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2323.

Summary

The PR is a focused, well-tested change that gates App Router <Link> and router.prefetch() prefetches behind isBotUserAgent. The gate placements are correct (scoped inside hasAppNavigationRuntime() / after isServer), vp check and both unit test files pass, and the E2E spec is a real regression guard with a valid header assertion.

One issue worth addressing — found by tracing all App Router RSC prefetch entry points:

  • packages/vinext/src/shims/form.tsx:311-369 — the <Form> viewport prefetch uses the same unified RSC prefetch path (createRscRequestUrl + prefetchRscResponse) as <Link> and router.prefetch(), but it has no bot gate. A bot scrolling a page with an App <Form> still triggers an RSC prefetch — the exact behavior this PR suppresses for <Link>. Suggested a one-line isBotUserAgent guard + a focused test.

This isn't strictly a Next.js parity miss (Next's App Form prefetch goes through the segment-cache scheduler, which this PR intentionally avoids), but it's an inconsistency within vinext's own prefetch model since all three call sites share the same mechanism.

Minor non-blocking notes on bot-list equivalence vs Next's isBot and the assertSafeNavigationUrl ordering (both acceptable). Core change is correct and safe to ship.

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.

Re-review: skip prefetches for bots

The earlier review's one substantive concern — the ungated next/form App Router prefetch — has been addressed in 7576a6f. All three App Router RSC prefetch entry points are now gated consistently, the change passes vp check and the three unit files (110 tests), and the E2E spec now covers Link and Form. Looks good to ship.

Verified

  • All three RSC prefetch entry points gated. Traced every createRscRequestUrl + prefetchRscResponse call site:

    • link.tsx:438 — viewport/intent prefetch (gated inside hasAppNavigationRuntime())
    • navigation.ts:1852router.prefetch() (gated after isServer)
    • form.tsx:328 — viewport prefetch (gated inside the IntersectionObserver async IIFE) ✅ new in 7576a6f

    The remaining createRscRequestUrl call sites in app-browser-entry.ts (1283, 1605, 2176) are navigation-commit and HMR-replace paths, not prefetch paths, so they're correctly left ungated — a bot that actually navigates must still receive real content.

  • Navigation preserved for bots. The E2E spec (prefetch-bot.browser.spec.ts:148-199) asserts no RSC: 1 prefetch on hover/viewport for a Googlebot UA, but exactly one RSC request on Link click and one on Form submit. This matches Next.js: only prefetch creation is gated, not navigation.

  • Pages Router prefetch intentionally preserved. link-navigation.test.ts adds preserves Pages Router viewport, explicit, and intent prefetches for a bot user agent, confirming the gate is scoped to hasAppNavigationRuntime() and Pages <link rel="prefetch"> document prefetches still emit for bots — matching Next.js, which only gates App Router createPrefetchURL.

  • form.tsx observer hygiene. observer.unobserve(node) runs outside the async IIFE, so even when the bot gate returns early the observer still detaches — no leaked observer.

Minor notes (non-blocking, all acceptable)

  • router.prefetch gate ordering. isBotUserAgent now runs before the URL-parse-throws block (navigation.ts:1862-1866). This actually matches Next's createPrefetchURL, which checks isBot before parsing — a bot passing an unparseable href silently returns rather than throwing, same as Next. assertSafeNavigationUrl still runs unconditionally first as vinext defense-in-depth, which is fine.
  • Bot-list equivalence. isBotUserAgent combines the HTML-limited list + the headless Googlebot regex and is documented to mirror Next's is-bot.ts. The combined list is a superset of Next's isBot for this use case.
  • window.navigator?.userAgent ?? "" — the optional chaining + empty-string fallback means a missing navigator/UA is treated as "not a bot" (prefetch proceeds), which is the safe default and matches isBotUserAgent("") === false.

Core change is correct, consistent across all vinext App Router prefetch entry points, and safe to ship.

github run

@ask-bonk

ask-bonk Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2323.

Summary

This PR gates all three App Router RSC prefetch entry points behind isBotUserAgent. The earlier review's only substantive concern — the ungated next/form viewport prefetch — has been fixed in commit 7576a6f, so the change is now complete and consistent.

Verified:

  • All three prefetch entry points gated: <Link> (link.tsx:438), router.prefetch() (navigation.ts:1852), and <Form> (form.tsx:328).
  • The remaining createRscRequestUrl call sites in app-browser-entry.ts are navigation-commit/HMR paths, not prefetches, so correctly left ungated — bots that navigate still get real content.
  • E2E spec asserts no prefetch on hover/viewport but exactly one RSC request on Link click and Form submit (matches Next.js).
  • Pages Router document prefetches intentionally preserved for bots (gate scoped to hasAppNavigationRuntime()), confirmed by a dedicated test.
  • vp check + 3 unit files (110 tests) pass locally.

Minor non-blocking notes: bot check ordering relative to URL-parse-throws actually matches Next's createPrefetchURL; bot-list is a superset of Next's isBot; missing-UA falls back to "not a bot" (safe default).

Conclusion: correct, consistent, and safe to ship — no blocking issues found.

github run

@james-elicx james-elicx marked this pull request as ready for review June 25, 2026 12:36
@james-elicx james-elicx merged commit c5bd7b3 into main Jun 26, 2026
58 checks passed
@james-elicx james-elicx deleted the codex/fix-bot-prefetch-parity branch June 26, 2026 18:59
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