A speculation rules script tells Chrome which links it should prerender before the user clicks, so the next page is already rendered when the click lands. You add a <script type="speculationrules"> block with a prerender rule, choose an eagerness level, and make sure the target page does nothing irreversible while it is hidden. Chrome is the only browser that acts on it as of October 2026; the others ignore the script and lose nothing.
What does a speculation rules prerender script look like?
The simplest form is a list of URLs. Chrome starts a prerender for each one as soon as it sees the script.
<script type="speculationrules">
{
"prerender": [{
"urls": ["/pricing", "/docs/getting-started"],
"eagerness": "immediate"
}]
}
</script>
That is fine for a landing page with one obvious next step. It is a poor fit for a site with hundreds of links, because you do not know which one the user wants. For that case use document rules, which match links already on the page:
<script type="speculationrules">
{
"prerender": [{
"where": {
"and": [
{ "href_matches": "/*" },
{ "not": { "href_matches": "/account/*" } },
{ "not": { "href_matches": "/cart*" } },
{ "not": { "selector_matches": ".no-prerender" } }
]
},
"eagerness": "moderate"
}]
}
</script>
href_matches takes a URL pattern, selector_matches takes a CSS selector, and and, or and not combine them. The exclusions matter more than the inclusion. Anything that changes state when it loads, which on a shop means the cart, checkout and sign-out URLs, has to be kept out.
The same JSON can be served by HTTP instead of inline. Send a Speculation-Rules: "/speculationrules.json" response header and serve that file with Content-Type: application/speculationrules+json. The header form shipped in Chrome 121 along with the eagerness field, according to the Chrome prerender documentation. It is the right choice when you cannot touch the HTML, for example through a CDN rule.
Which eagerness setting should you use?
eagerness is the trigger. Chrome documents four values. immediate speculates the moment the rules are parsed. eager fires after a 10ms hover on desktop or 50ms after a link enters the viewport on mobile. moderate waits for a 200ms hover on desktop and uses viewport heuristics on mobile. conservative waits for pointer down or touch start.
The defaults differ by rule type: list rules default to immediate, document rules to conservative.
Chrome’s limits decide the choice as much as the triggers do. It caps immediate prerenders at 10 and immediate prefetches at 50. For eager, moderate and conservative the cap is 2 of each, first in, first out, so a hover on a third link evicts the oldest prerender. Rendering a page costs the user bandwidth, CPU and battery, and Chrome will cancel speculation altogether under Save-Data, Energy Saver or low memory, so a rule that fires too readily wastes work on exactly the devices that can least afford it.
My default for a content site is moderate for prerender and eager for prefetch, each in a separate rule. A prefetch costs only the HTML document, so it can be greedier. The prerender then upgrades the two most likely clicks.
<script type="speculationrules">
{
"prefetch": [{
"where": { "href_matches": "/blog/*" },
"eagerness": "eager"
}],
"prerender": [{
"where": { "href_matches": "/blog/*" },
"eagerness": "moderate"
}]
}
</script>
The 200ms hover is Chrome’s reading of intent. A cursor crossing a link on the way somewhere else does not hold still for that long; a cursor that has stopped on it probably means a click, and the prerender gets the gap between the pause and the click to do its work. If your pages are heavy and activations still feel slow, move to eager and accept the extra waste.
What must a prerendered page not do?
This is the part that goes wrong in production. A prerendered page runs its JavaScript in a hidden document that the user may never see. Anything with a side effect on load fires before, and possibly instead of, a real visit.
MDN’s Speculation Rules API page lists the cases to exclude outright: sign-out URLs, language switchers, add-to-cart links, sign-in flows that send an SMS code, anything that increments a usage allowance, and anything that starts server-side conversion tracking. Keep those out with not rules.
For everything else, the page needs to know it is being prerendered. document.prerendering is true while the document is hidden, and the prerenderingchange event fires once on activation. Analytics and anything else that should count as a visit goes behind that:
function initAnalytics() {
// page view, ad impressions, A/B assignment
}
if (document.prerendering) {
document.addEventListener('prerenderingchange', initAnalytics, { once: true });
} else {
initAnalytics();
}
The same guard belongs around writes to localStorage or IndexedDB on load, because a hidden page that overwrites state the visible page relies on is a bug that only reproduces for users with the feature enabled.
On the server, every speculative request carries a Sec-Purpose header: prefetch for a prefetch, prefetch;prerender for a prerender. You can log on it, serve different content, or return an error to refuse the speculation. A quick win is to check it in whatever middleware records page views, so a prerender that is never activated does not count as traffic.
Timing also needs care. performance.getEntriesByType('navigation')[0].activationStart tells you when the user actually arrived. If you compute LCP or TTFB yourself, subtract it, or a prerendered page reports a load time of nothing and your dashboards look better than the experience was. INP is unaffected, since it measures interactions after activation; our write-up on fixing Interaction to Next Paint still applies as written.
Where does it not work?
As of October 2026 this is a Chromium feature. MDN marks it as limited availability and not part of Baseline. Safari has an implementation in WebKit behind a flag in Safari Technology Preview, and Mozilla has published a positive position on the API, but neither has shipped it to users. Browsers that do not understand the script type ignore it, so there is no breakage, only no benefit.
Prerendering is same-origin only by default. A same-site but cross-origin target (say shop.example.com from www.example.com) has to opt in with a Supports-Loading-Mode: credentialed-prerender response header. Cross-site prerendering is not possible at all.
Feature detection, if you want to fall back to <link rel="prefetch"> elsewhere:
if (HTMLScriptElement.supports?.('speculationrules')) {
// inject the speculation rules script
} else {
const link = document.createElement('link');
link.rel = 'prefetch';
link.href = '/next';
document.head.append(link);
}
A single-page app gains little here, because its router already prefetches route data and the document never reloads. Speculation rules are for multi-page sites: content, documentation, and shops built on server-rendered templates. Paired with cross-document view transitions, an MPA gets the feel of an SPA without shipping one.
What I would do
Start with a prefetch rule at eager across the whole site, excluding account, cart and checkout paths. That costs almost nothing and removes server latency from every navigation. Measure for a week. Then add a prerender rule at moderate for the paths where a full render is the slow part, usually product and article pages, after you have guarded analytics with document.prerendering and checked your middleware for Sec-Purpose.
I would not prerender on a site that mutates state in page load handlers and cannot be audited for it, and I would not go to immediate for anything other than a single, near-certain next page. The prerender limit is 10 and the user’s data plan is finite.
Whoooop does this kind of work as part of website speed optimisation: auditing what a page does on load, deciding which navigations are worth prerendering, and proving the gain in field data rather than in a lab run.