The Speculation Rules API lets a page tell the browser which links to fetch, or fully render, before anyone clicks them. You declare rules in a <script type="speculationrules"> block, or point at a JSON file with a Speculation-Rules response header. Prefetch downloads the next page’s HTML. Prerender builds the whole page in a hidden tab. Prefetch is the safer default.
The appeal for a multi-page site is obvious. You get the instant-navigation feel that people install a client-side router to buy, without shipping the router. Pair it with cross-document view transitions and a plain server-rendered site gets most of what an SPA promised.
What does the Speculation Rules API actually do?
There are two rule shapes. A list rule names URLs explicitly:
<script type="speculationrules">
{
"prerender": [
{ "urls": ["/pricing", "/about"] }
]
}
</script>
A document rule matches links already present in the page, so you never maintain a URL list:
<script type="speculationrules">
{
"prefetch": [
{
"where": {
"and": [
{ "href_matches": "/*" },
{ "not": { "href_matches": "/logout" } },
{ "not": { "selector_matches": ".no-prefetch" } }
]
},
"eagerness": "moderate"
}
]
}
</script>
href_matches takes a URL pattern, selector_matches takes a CSS selector, and and, or and not compose them. Chrome made the source key optional from version 122, since it can infer "list" from urls and "document" from where. Chrome’s improvements post covers that alongside the Speculation-Rules header and expects_no_vary_search.
Speculative requests carry a Sec-Purpose header, so your server can identify them: Sec-Purpose: prefetch for a prefetch, Sec-Purpose: prefetch;prerender for a prerender. Filter on it in your access logs, or your traffic graphs will start lying to you.
Prefetch or prerender: which should you ship?
Prerender is the one that feels like magic and the one that will bite you. The browser runs the target page for real. Your JavaScript executes, your analytics fires, your effects run, your session recorder starts recording. All of that happens for a page the user may never open.
Shopify ran into exactly this. When they rolled speculation rules out platform-wide they chose prefetch, in their own words to avoid analytics overreporting, and they measured “an average 130ms improvement on desktop and 180ms on mobile across all percentiles, and across all loading metrics”. That is prefetch alone, at conservative eagerness, across one of the largest commerce footprints on the web.
130ms is not a demo-day number. It is the sort of gain you would otherwise chase through a month of build work.
Prerender earns its place when the next page is expensive and predictable. The checkout step after “add to basket”. The first results page after a search box. Somewhere you can name the destination in advance and you have audited what runs on load.
How do the eagerness values decide when to fire?
Four values, and the whole difference between them is the trigger.
immediate speculates the moment the browser parses the rule. eager fires after a 10ms hover on desktop, or 50ms after the link enters the viewport on mobile. moderate waits for a 200ms hover or a pointerdown on desktop, and on mobile fires 50ms after scrolling stops for links near the viewport. conservative waits for pointer or touch down, which sounds useless until you remember the gap between mousedown and the navigation committing is often 100ms or more of free head start.
The defaults differ by rule type, which catches people out. List rules default to immediate. Document rules default to conservative.
Chrome also caps how many speculations it holds. immediate and eager get 50 prefetches and 10 prerenders. moderate and conservative get two of each, evicted first in, first out, so a user sweeping across a nav bar continuously replaces the oldest candidate. Those limits are in Chrome’s prerender guide.
Use moderate for prefetch on a content site. Use conservative where bandwidth cost matters, which on a mobile-heavy storefront it does. Reserve immediate for a list rule with two or three URLs you are genuinely confident about.
Which URLs must you exclude?
Anything that changes state on a GET. Logout is the standard example and the standard incident. A conservative prefetch of /logout is harmless, but an eager document rule matching every link on the page will sign users out while they read.
Exclusion is a not clause:
{
"prefetch": [{
"where": {
"and": [
{ "href_matches": "/*" },
{ "not": { "href_matches": "/logout" } },
{ "not": { "href_matches": "/cart/*" } },
{ "not": { "href_matches": "/admin/*" } }
]
},
"eagerness": "moderate"
}]
}
Staleness is the harder half. A prefetched page is a snapshot. If someone adds an item to their basket after that snapshot was taken, the header on the “instant” page shows the old count.
Chromium extends Clear-Site-Data to deal with it. Return this from any same-site endpoint that mutates state:
Clear-Site-Data: "prefetchCache", "prerenderCache"
Shopify sends it from /cart/update, /cart/add and the rest of their cart-mutating endpoints. The two values are separately useful: a client-rendered app might clear prerenderCache on a state change while keeping prefetchCache, because the HTML shell is still good and only the rendered state went stale. Both values are a Chromium extension rather than part of the W3C Clear-Site-Data specification, so treat them as an optimisation rather than a guarantee.
How do you stop prerendering from wrecking your analytics?
Gate anything with a side effect on activation. document.prerendering is true while the page is being built in the background, and a prerenderingchange event fires when the user actually arrives.
function startAnalytics() {
// page view, session recording, experiment assignment
}
if (document.prerendering) {
document.addEventListener('prerenderingchange', startAnalytics, { once: true });
} else {
startAnalytics();
}
Timing is the second problem. A prerendered page’s performance entries are measured from when prerendering began, not from when the user arrived, so your LCP looks impossibly good. PerformanceNavigationTiming.activationStart gives you the offset to subtract:
const nav = performance.getEntriesByType('navigation')[0];
const activationStart = nav?.activationStart ?? 0;
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// Remove the prerender head start so the figure reflects
// what the user actually waited for.
const lcp = Math.max(0, entry.startTime - activationStart);
console.log('LCP relative to activation:', lcp);
}
}).observe({ type: 'largest-contentful-paint', buffered: true });
The web-vitals library already applies this correction. If you built your own RUM collector, you have to do it yourself, and until you do, prerendering will quietly turn your dashboard into fiction. The same trap shows up from the interaction side in our write-up on diagnosing and fixing INP.
Does it work outside Chrome?
Not yet, and it is worth planning for that honestly.
Chrome and Edge have supported speculation-rules prerendering since 109, and the eagerness values since 121. Firefox’s standards position is neutral, tagged with a complexity concern, and nothing has shipped. WebKit’s position issue is still open with no position recorded, although Safari Technology Preview builds have been fixing speculation rules bugs through 2026, which suggests real work in progress. MDN still marks the API as not Baseline. As of August 2026, treat it as a Chromium enhancement.
That is workable, because it degrades to nothing. A browser that does not understand <script type="speculationrules"> ignores the block. No polyfill, no fallback path, no second code path to keep alive, which makes the cost-benefit unusually easy compared with most performance work.
What I would ship
Start with a document rule, prefetch only, moderate eagerness, with not clauses for logout, cart mutations and every admin path. Keep the rules in the HTML while you are iterating; the Speculation-Rules: "/rules.json" header form caches better at the CDN but is slower to change. Add Clear-Site-Data: "prefetchCache" to every state-mutating endpoint on day one, rather than after the first stale-basket bug report.
Only then consider prerender, and only for one or two named destinations where you have read every line of what runs on load. If you cannot say with confidence what your tag manager does on page load, you are not ready to prerender, and prefetch was already giving you most of the win.
Skip it altogether if your navigations are already quick, or if your traffic is overwhelmingly Safari. Speculation rules make a fast site feel instant. They will not rescue a page that takes two seconds to render on the server, and that is a page speed problem to fix first.