Speculation Rules API: instant navigation without a framework

The reason single-page apps got popular was never the routing — it was that clicking a link didn't feel like a full stop. The Speculation Rules API gives a plain multi-page site the same feel with about ten lines of JSON and no framework at all. The catch is that prerender means your next page really runs, scripts and all, before the user has decided to go there. This is the practical version: what to declare, what to exclude, and how to keep your analytics and your side effects honest.

Two mechanisms with very different costs

Rules go in a <script type="speculationrules"> block (or an HTTP header, later) and come in two flavours:

  • prefetch — the browser downloads the response body of the target document and nothing else. No subresources, no JavaScript execution. It removes server time and connection setup from the next navigation. Cheap enough that MDN explicitly encourages applying it broadly across a site's significant pages.
  • prerender — the browser fetches and renders the page into an invisible tab: subresources load, scripts execute, and fetches those scripts kick off happen too. Navigating then swaps in a finished document, which is as close to instant as the web gets. You pay memory and bandwidth for every guess you get wrong, and prerendered URLs are prefetched as well.

It's a document-level API, keyed on URLs, so it's for MPAs — a client-routed SPA has nothing to speculate about. It also supersedes two older things: <link rel="prefetch"> (which the browser treats as a generic resource hint, not a navigation) and Chrome's never-standardised <link rel="prerender">, which is being sunset.

current page speculationrules document rule eagerness: moderate hidden prerender HTML + subresources scripts execute document.prerendering activated prerenderingchange send pageview here LCP ≈ 0 ms hover 200 ms click not activated → discarded on navigation away: bandwidth + memory spent for nothing excluded by rule → /logout, ?add-to-cart=, .no-prerender, [rel~=nofollow] unsupported browser → plain navigation, no error

List rules, then document rules

The simplest form is a hand-written list. Good for the one or two pages you are sure about:

<script type="speculationrules">
{ "prerender": [{ "urls": ["/checkout/", "/pricing/"] }] }
</script>

Maintaining lists by hand doesn't scale, so Chrome 121–122 added document rules: the browser harvests candidate URLs from the links in the page itself and filters them with a where condition. This is the form worth deploying site-wide:

{
  "prerender": [{
    "source": "document",
    "where": { "and": [
      { "href_matches": "/*" },
      { "not": { "href_matches": "/logout*" } },
      { "not": { "selector_matches": ".no-prerender" } },
      { "not": { "selector_matches": "[rel~=nofollow]" } }
    ] },
    "eagerness": "moderate"
  }]
}

href_matches takes URL patterns; selector_matches takes CSS selectors, which is the useful escape hatch — put class="no-prerender" on any anchor you don't want touched and the rule stops applying, no rule edit needed.

Eagerness: the precision/lead-time dial

Every speculation trades waste against lead time. eagerness separates when to speculate from which URLs, and Chrome's heuristics are:

  • immediate — as soon as the rules are parsed. Default for list rules.
  • eager — currently identical to immediate; reserved to sit between immediate and moderate later.
  • moderate — after 200 ms of hover, or on pointerdown if that comes first (and on pointerdown on touch devices, where there is no hover).
  • conservative — on pointer or touch down. Default for document rules.

Sensible defaults: moderate for a site-wide prerender document rule, immediate for a tiny prefetch list of near-certain next pages, and conservative if you are nervous or serving expensive pages. Chrome also caps how many speculations are in flight (roughly two prerenders for moderate/conservative, ten immediate ones), discarding on a first-in-first-out basis, so you cannot accidentally prerender fifty links.

The part people skip: making pages prerender-safe

A prerendered page runs. If your page code counts a view, starts a video, writes to localStorage, or posts a "user reached step 3" event on load, that now happens for visits that never occur. Two APIs fix this:

if (document.prerendering) {
  document.addEventListener('prerenderingchange', init, { once: true });
} else {
  init(); // normal load, or already activated
}

Anything with a side effect goes inside init. You can also read performance.getEntriesByType('navigation')[0].activationStart to see how long the document sat prerendering — non-zero means it was activated from a prerender. Google Analytics 4 and Google Publisher Tag already defer to activation on their own; hand-rolled tracking, A/B test bucketing and consent logic do not, and that is where inflated pageviews come from.

Beyond scripts, exclude any URL where a plain GET changes state — /logout, ?add-to-cart=, one-time magic links, anything paginated with a server-side cursor. If a GET request mutates data, prerendering will find that bug for you, in production. (Same principle as designing for retries: see the notes on idempotency and webhooks.)

Headers, CSP and cross-origin limits

You don't need an inline script. A response header can point at a JSON file: Speculation-Rules: "/rules/prefetch.json", where that file is served as application/speculationrules+json — a same-origin URL, and the wrong MIME type is a silent failure. Inline and header rules both apply and are merged.

If you send a Content-Security-Policy with script-src, the inline form needs 'inline-speculation-rules', a hash or a nonce, or the block is ignored. Origin limits are firm: same-origin prerender by default, cross-origin same-site prerender only if the target opts in with Supports-Loading-Mode: credentialed-prerender, and cross-site prerender not at all. Cross-site prefetch is possible but anonymous — add "requires": ["anonymous-client-ip-when-cross-origin"] and expect no cookies.

Support in 2026, and how to check your work

Chrome and Edge 109+ have the full implementation, with document rules and eagerness from 121–122 — roughly four-fifths of global traffic. Firefox Nightly has shipped limited support since Firefox 155: same-origin prefetch only, list rules and immediate eagerness. Safari has an implementation but not enabled by default. This is a progressive enhancement — an unsupported browser just navigates normally — so a fallback is optional:

if (!HTMLScriptElement.supports?.('speculationrules')) {
  /* inject <link rel="prefetch"> for a few key URLs */
}

To verify: Chrome DevTools → Application → Speculative loads lists every candidate with its status and, crucially, a reason when one was rejected or discarded. Watch it while hovering links, and check your analytics realtime view for phantom views before you ship the rule site-wide. Measure with field data rather than a lab score — a prerendered navigation reports an LCP near zero, which flatters a synthetic test that never had to guess.

A short deployment checklist

  1. Start with prefetch, document source, conservative. Nothing runs; nothing breaks.
  2. Exclude state-changing GETs by pattern, plus a .no-prerender selector escape hatch.
  3. Guard every load-time side effect behind document.prerendering + prerenderingchange.
  4. Upgrade the rule to prerender with moderate, on one template first.
  5. Add 'inline-speculation-rules' to CSP, or move rules to the header.
  6. Check DevTools speculative loads for rejection reasons, and analytics for inflated counts.

Done in that order it's a genuinely rare thing on the modern web: a large, obvious speed win that costs you a JSON blob, a CSP line and one if statement — no build step, no framework, no rewrite.

Related
→ View Transitions API for multi-page apps → WebSocket vs SSE vs WebTransport → SharedArrayBuffer and Atomics → Portfolio & projects
What is the difference between prefetch and prerender in speculation rules?

Prefetch downloads only the target document's response body — no subresources, no script execution — so it mainly removes server and connection time. Prerender fetches and fully renders the page in a hidden tab: subresources load, JavaScript runs, and fetches started by that JavaScript run too. Navigation is then near-instant, but you pay memory and bandwidth for wrong guesses, and load-time side effects happen before the user commits.

Which eagerness setting should I use?

moderate for a site-wide prerender document rule: Chrome speculates after 200 ms of hover, or on pointerdown if sooner. conservative (the document-rule default) fires on pointer/touch down and wastes almost nothing. immediate (the list-rule default) suits a short hand-picked list of near-certain next pages. eager currently behaves like immediate.

Does prerendering break analytics?

It can inflate pageviews, because a prerendered page runs its scripts before — and possibly without — a visit. GA4 and Google Publisher Tag already defer until activation; custom tracking does not. Wrap it: if document.prerendering is true, wait for the prerenderingchange event on document, then send.

Which browsers support the Speculation Rules API in 2026?

Chrome and Edge 109+ (document rules and eagerness from 121–122) — about 79% of global traffic. Firefox Nightly has limited support since Firefox 155: same-origin prefetch, list rules, immediate only. Safari has an implementation behind a flag. Unsupported browsers navigate normally, so no fallback is required; feature detect with HTMLScriptElement.supports('speculationrules') if you want one.

Can I prerender pages on another domain?

Not cross-site. Same-origin prerendering works by default; cross-origin same-site prerendering needs the target to send Supports-Loading-Mode: credentialed-prerender. Cross-site prefetch is allowed only anonymously, via "requires": ["anonymous-client-ip-when-cross-origin"], so no cookies are sent.