Skip to content
All posts
#Performance#Architecture#CSS#React

Instant Navigation Without a Router: The Speculation Rules API

7 min read

How the Speculation Rules API lets the browser prefetch or prerender the page a visitor is about to open — and what your code must stop doing to be safe to render ahead of time.

Single-page apps bought instant navigation by taking over routing, paying for it with hydration, bundle size, and a router that reimplements scrolling, focus, and the back button. The Speculation Rules API offers a different trade: let the browser guess which document you open next and prepare it before you click. MDN frames it as targeting document URLs rather than resource files, a fit for multi-page applications rather than SPAs.[1]

It is not a drop-in win. Speculation rules are hints the browser may ignore, and every wrong guess spends the user's bandwidth, memory, and CPU.[11][5] Getting it right means knowing what each action costs, when the browser fires it, and what your pages must stop doing to be safe to render ahead of time.

Two Actions With Very Different Price Tags

The rules live in a JSON block inside a <script type="speculationrules"> element, or in a separate file referenced by the Speculation-Rules response header.[1] That JSON may contain at most three keys — tag, prefetch, and prerender — and prerender may be converted to prefetch at parse time by implementations that choose to.[9]

prefetch downloads the response body of the target page and nothing else; subresources are not fetched and JavaScript is not executed.[1] The HTML is cached before the user arrives, so the round trip disappears but parse, render, and script execution still happen after the click. MDN recommends adopting prefetch broadly precisely because that cost is small.[1] The result lives in a per-document in-memory cache discarded on navigation, and Chrome keeps prefetched pages for around five minutes.[1]

prerender goes much further. The browser fetches, renders, and loads the page into an invisible tab — subresources, JavaScript, and even data fetches started by that JavaScript run — and activating it later is close to instant.[1] MDN puts the cost bluntly: a prerender uses about the same resources as rendering an <iframe>.[1] Prerendering is same-origin by default; cross-origin same-site prerendering needs the target to opt in with Supports-Loading-Mode: credentialed-prerender, and cross-site prerendering is impossible.[1]

The practical rule that falls out of this asymmetry: prefetch widely, prerender narrowly.

List Rules and Document Rules

A list rule names its URLs explicitly, which suits the one or two destinations you are confident about — a checkout step, the next page of a series.[2] Document rules instead harvest candidate URLs from the anchors already in the page and filter them with a where condition built from href_matches and selector_matches, combined with and, or, and not.[2] That is the form worth deploying site-wide, because one ruleset can serve the whole site instead of being regenerated per page.[6]

The selector_matches escape hatch is what makes document rules operable. It evaluates a CSS selector against the anchor element, so an opt-out class such as .no-prerender disables speculation for a link without touching the rule.[2] Exclusions matter more than inclusions: sign-out URLs, language switchers, "add to cart" URLs, and sign-in flows that trigger an SMS one-time password are all documented as unsafe to prefetch, because the speculative GET itself performs the server-side side effect.[1]

Eagerness Is the Only Dial That Really Matters

Eagerness separates *which* URLs to speculate on from *when*, and the defaults are deliberate: list rules default to immediate, while document rules default to conservative, because a document can contain a great many links.[2][6] The four values are hints the browser weighs against its own heuristics — it may pick a less eager candidate over a more eager one if it judges that candidate better.[2]

Chrome's heuristics define what the hint means in practice. conservative speculates on pointer or touch down; moderate after the pointer rests on a link for 200 milliseconds, or on pointerdown if that comes first; immediate as soon as the rules are observed.[5] eager no longer matches immediate — as of Chrome 143 it fires after a 10-millisecond hover on desktop.[5] For most content sites, a site-wide document rule at moderate is the sensible starting point.[5]

Chrome also caps how much speculation can be in flight, the guardrail that makes over-eager rules survivable. At immediate eagerness it allows 50 prefetches and 10 prerenders; at the other three levels the limit drops to 2 of each, managed as a first-in-first-out queue where a new speculation evicts the oldest.[5] Eviction is cheap, since the earlier attempt has usually warmed the HTTP cache for that URL.[5] Speculation is suppressed entirely under Save-Data, energy saver on low battery, memory pressure, and when "Preload pages" is off — as extensions such as uBlock Origin do.[5]

Making a Page Safe to Prerender

A prerendered page runs. If your code counts a page view, starts a video, or writes to storage on load, that now happens for visits that never occur. MDN lists storage writes on load and analytics or ad-impression side effects among the conditions that make a page unsafe to prerender.[1]

Two APIs defer that work. Document.prerendering is true while the document is being prerendered, and the prerenderingchange event fires when the page is actually activated.[1][4] The standard pattern gates every load-time side effect behind those two, running it immediately on a normal load and on activation otherwise.[1]

Servers can detect speculation before any of this runs: speculative requests carry a Sec-Purpose header, prefetch for a prefetch and prefetch;prerender for a prerender.[1] That lets an origin skip analytics, rate-limit buckets, or access logging for requests a user may never follow through on, and any non-2xx status prevents the speculation entirely.[1]

Two smaller details bite people. Inline rules need explicit permission from a Content-Security-Policy script-src directive via 'inline-speculation-rules', a hash, or a nonce, or the block is silently ignored.[1] And the JSON file referenced by the Speculation-Rules header must be served as application/speculationrules+json.[1]

Support, and What to Ship

Treat this as progressive enhancement. MDN marks the Speculation Rules API as limited availability, not Baseline, and experimental technology.[1]

MDN's compat data records Chrome 109 for type="speculationrules" (partial support back to 105, same-origin prerendering only) and Chrome 121 for eagerness.[10] Firefox has no support; Safari's prefetch entry starts at 26.2 behind a preference flag named "SpeculationRules prefetch", with eagerness marked partial.[10] Unsupported browsers ignore the script element, so shipping it cannot break them.[1]

Chrome is actively extending the API. The prerender_until_script addition, in origin trial from Chrome 144, sits between the two actions: it prefetches the HTML and renders the page, fetching subresources, but avoids executing <script> elements, pausing the parser at a blocking script until the user navigates.[8] It remains subject to change and needs opting in.[8]

A rollout that respects the constraints looks like this. Start with a prefetch document rule at conservative eagerness, which MDN calls the broadly recommended adoption path and Chrome's implementation guide calls the initial approach taken by large rollouts.[1][3][7]

Exclude state-changing GET URLs by pattern and add a .no-prerender escape hatch.[2]

Gate every load-time side effect behind document.prerendering and prerenderingchange, and move analytics behind the same check.[1] Then promote one high-intent template to prerender at moderate, and confirm in DevTools that the speculations you asked for are the ones firing.[5]

Deploying the rules through the Speculation-Rules header is the option to reach for when a CDN must add them without altering document contents.[7] The old <link rel="prerender"> is deprecated; <link rel="prefetch"> remains useful for subresources, which the Speculation Rules API does not handle.[3] The two are complementary, not competing.

Sources

[1] https://developer.mozilla.org/en-US/docs/Web/API/Speculation_Rules_API — Speculation Rules API - MDN Web Docs

[2] https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script/type/speculationrules — script type=speculationrules - MDN Web Docs

[3] https://developer.mozilla.org/en-US/docs/Web/Performance/Guides/Speculative_loading — Speculative loading - MDN Web Docs

[4] https://developer.mozilla.org/en-US/docs/Web/API/Document/prerendering — Document: prerendering property - MDN Web Docs

[5] https://developer.chrome.com/docs/web-platform/prerender-pages — Prerender pages in Chrome for instant page navigations - Chrome for Developers

[6] https://developer.chrome.com/blog/speculation-rules-improvements — Improvements to the Speculation Rules API - Chrome for Developers

[7] https://developer.chrome.com/docs/web-platform/implementing-speculation-rules — Guide to implementing speculation rules for more complex sites - Chrome for Developers

[8] https://developer.chrome.com/blog/prerender-until-script-origin-trial — Speculation rules prerender until script origin trial - Chrome for Developers

[9] https://html.spec.whatwg.org/multipage/speculative-loading.html — Speculative loading - HTML Standard (WHATWG)

[10] https://raw.githubusercontent.com/mdn/browser-compat-data/main/html/elements/script.json — browser-compat-data html/elements/script.json (speculationrules) - MDN

[11] https://raw.githubusercontent.com/web-platform-dx/web-features/main/features/speculation-rules.yml — web-features: speculation-rules - web-platform-dx