Speculation Rules Give MPAs Instant Navigations — and Phantom Pageviews
For a decade the deal was simple. Want instant page loads? Build a single-page app. Pay the tax: a client-side router, a bigger bundle, hydration on every screen. Multi-page sites accepted the white flash between navigations as the cost of plain HTML.
The Speculation Rules API changes that deal. A page declares, in a block of JSON, which links are worth loading ahead of time. When the browser sees an intent signal, it fetches the next page, or fully renders it in an invisible tab, before the click ever lands. You get SPA-perceived speed on a real multi-page architecture, with none of the router weight.
Chrome and Edge have shipped it for years. Firefox started shipping a slice in Nightly this year, and Safari has an implementation disabled by default. Unsupported browsers ignore the JSON block entirely, no errors, no fallback code. It also carries one trap that bites almost every team on their first rollout: numbers on a dashboard that shouldn't be there.
Three actions, three price tags
prefetch downloads the response body of the target document. No subresources, no JavaScript, no rendering. It deletes server time and connection setup from the next navigation and costs roughly one page of bandwidth. Cheap enough to apply broadly.
prerender does the whole thing: loads subresources, executes JavaScript, and lays out the page in a hidden tab. When the user clicks, the browser swaps the finished document in. Prerendered navigations routinely report near-zero LCP, and you pay memory and bandwidth for every wrong guess.
prerender_until_script landed in Chrome 144 in January 2026 and is the interesting newcomer. It renders the page and loads CSS, images, and fonts, then pauses JavaScript at the first blocking script tag: you pay the render cost up front while dodging the side effects of running a page nobody visited. It's still in W3C TAG review, so treat it as Chromium-only for now.
This supersedes Chrome's never-standardized <link rel="prerender">, which is being sunset. One practical fix along the way: the old tag often got blocked by Cache-Control settings, and speculation rules aren't.
The rules are just JSON
Rules go in a <script type="speculationrules"> block. A hand-written list is good for the two or three pages you're sure about:
<script type="speculationrules">
{
"prerender": [
{ "urls": ["/pricing", "/docs/getting-started"],
"eagerness": "moderate" }
]
}
</script>Lists don't scale, so Chrome 121 added document rules: the browser harvests candidate URLs from the links in the page and filters them with a where condition tree.
<script type="speculationrules">
{
"prefetch": [
{
"where": {
"and": [
{ "href_matches": "/*" },
{ "not": { "href_matches": "/logout" } },
{ "not": { "selector_matches": ".no-prerender" } }
]
},
"eagerness": "conservative"
}
]
}
</script>selector_matches is the escape hatch I like most. Put class="no-prerender" on any anchor you don't want touched and the rule stops applying.
Rules can also ride in a Speculation-Rules response header pointing at a JSON file, which is how Shopify runs platform-wide rules on all Liquid storefronts. The file must be same-origin and served as application/speculationrules+json; the wrong MIME type fails silently. With a strict Content-Security-Policy, inline rules need 'inline-speculation-rules', a hash, or a nonce.
Eagerness is the whole tuning dial
Every speculation trades wasted bytes against lead time, and eagerness sets that trade independently of which URLs you match.
| Level | Desktop trigger | Chrome cap |
|---|---|---|
| immediate | As soon as the rule parses | 50 prefetch / 10 prerender |
| eager | Slightest intent, like a pointer moving toward a link | 50 prefetch / 10 prerender |
| moderate | ~200ms hover, or pointerdown | 2 each, FIFO |
| conservative | Pointerdown or touchstart only | 2 each, FIFO |
List rules default to immediate, document rules to conservative. Touch devices got their own triggers in January 2026: with no hover to wait for, eager fires shortly after a link enters the viewport and moderate drops to pointerdown. Chrome also refuses to speculate when Save-Data is on, when Battery Saver kicks in, or when the user has turned off page preloading. You can't override those from your rules.
Shopify's numbers make the trade concrete: moving storefronts from conservative to moderate raises the prefetch rate by roughly 0.7 to 1.0 percentage points from a 12 to 14% baseline, at the cost of 2 to 4% more requests to the origin. Moderate is the sane default for document rules. Reserve immediate for short lists of near-certain next pages, like the payment step behind a checkout button.
The phantom pageview problem
Here's the trap. A prerendered page runs. Its JavaScript executes, so the pageview counter fires, the onboarding video starts, the "user reached step 3" event posts, and your dashboard fills with visits that never happened. Ship prerender site-wide without a guard and conversion rates drop overnight.
The fix is small and mandatory. Guard side-effecting initialization with document.prerendering, true while the page sits hidden:
if (document.prerendering) {
document.addEventListener('prerenderingchange', initAnalytics, { once: true });
} else {
initAnalytics();
}The prerenderingchange event fires the moment the user actually views the page, so analytics, autoplay, and beacon posts wait for a real human. Servers can participate too: every speculative request carries a Sec-Purpose header (prefetch, or prefetch;prerender). A route that must never be speculated can return 503 to cancel that candidate without touching real navigations; a 204 or 205 blocks prerender while still allowing prefetch. Usually it's better to serve the page and defer the side effects in JavaScript.
Support reality, October 2026
Chrome and Edge ship the full API: prerender from 105, prefetch from 110, document rules and eagerness from 121. That's roughly four-fifths of global traffic. Firefox 155 started shipping limited support in Nightly: same-origin prefetch, list rules, immediate eagerness. Safari 26.2 ships an implementation disabled by default, prefetch but no prerender. Until those land, this is a Chromium feature with two engines warming up, which is what progressive enhancement is for. Feature-detect with HTMLScriptElement.supports('speculationrules') and keep a <link rel="prefetch"> fallback for the stragglers.
Verification is genuinely good: Chrome DevTools has an Application panel called Speculative loads that lists every candidate URL with its status and why it was rejected. Watch your analytics realtime view for phantom views for a week before widening the rules, and measure with field data: a prerendered navigation reports an LCP near zero, which flatters synthetic tests that never had to guess.
When to leave it alone
Skip this API if you run a client-routed SPA: there are no document navigations to speculate, so the rules match nothing. Be careful with pages whose load does real work, like expensive server-side personalization, cart mutations via GET links, or session-sensitive dashboards. Those get prefetch, or exclusion via selector_matches. Cross-origin is mostly off the table: prerender is same-origin by default, cross-origin same-site needs a Supports-Loading-Mode: credentialed-prerender opt-in from the target, and cross-site prerender doesn't exist.
The SPA-versus-MPA argument was always about who pays for perceived speed: your bundle, or the user's patience. Speculation rules move the bill to the browser's idle time, which was free all along. Start with one document rule, prefetch at conservative eagerness, watch the DevTools panel and your analytics for a week, then raise eagerness where the hit rate earns it.
Comments