Speculation Rules API: Make Page Navigations Instant — ContentBuffer guide

Speculation Rules API: Make Page Navigations Instant

K
Kodetra Technologies··8 min read Intermediate

Summary

Prerender and prefetch pages with the browser-native Speculation Rules API for instant loads.

Users judge a site in milliseconds. Even a well-optimized page that takes 800ms to load on the next click feels sluggish compared to a navigation that appears instantly. For years the only way to get that “instant” feel was a Single Page Application that hijacked routing in JavaScript — complex to build, heavy to ship, and prone to breaking the back button. The Speculation Rules API gives you the same instant feel on a plain multi-page site, using nothing but a small block of JSON the browser reads natively.

This guide teaches you how to prefetch and prerender the right pages at the right moment, how to tune how aggressively the browser speculates, and how to avoid the real-world traps — double-counted analytics, wasted bandwidth, and accidentally triggering destructive links. By the end you will be able to add near-zero-latency navigation to an existing site without adopting a framework or shipping a router.

Why now: the API is shipping in all Chromium browsers (Chrome, Edge, Opera), document-level rules with hover triggers are stable, and Google has confirmed prerendered pages count toward Core Web Vitals at activation time — so this is a genuine SEO and UX win, not an experiment.

Prerequisites

  • A multi-page website you control (any stack — static HTML, Rails, Django, WordPress, Astro).
  • Ability to add a <script> tag to your HTML, or set an HTTP response header.
  • A Chromium-based browser (Chrome 121+) for testing. Other browsers safely ignore the rules.
  • Basic comfort reading JSON. No build step or npm package is required.

What the Speculation Rules API actually does

You hand the browser a set of rules describing which URLs it may load ahead of time and one of two actions to take:

  • prefetch — the browser downloads the next page’s HTML document in the background and stores it in memory. When the user clicks, the response is already there, so the network round trip is skipped. JavaScript on the target page does not run yet.
  • prerender — the browser fully loads and renders the page in a hidden tab: it runs the page’s scripts, fetches subresources, and builds the DOM. On click, that hidden tab is instantly swapped in. Navigation is effectively 0 ms.

Prefetch is cheap and low-risk. Prerender is more powerful but more expensive, because the browser does the full work of loading a page the user might never visit. Choosing between them — and controlling when the browser acts — is the heart of using this API well.

Step 1 — Your first speculation rule

Add a single inline script anywhere in your HTML. Its type is speculationrules, and its body is JSON. This example tells the browser it may prerender two specific URLs:

<script type="speculationrules">
{
  "prerender": [
    {
      "source": "list",
      "urls": ["/pricing", "/docs/getting-started"]
    }
  ]
}
</script>

That is the entire setup. A list rule names URLs explicitly. Open Chrome DevTools → ApplicationSpeculative loads to watch the status move through PendingRunningReady. Click one of the links and the page appears with no visible load. If you measure it, you will see something like this:

// Before speculation rules (cold click)
performance navigation -> 740 ms to first contentful paint

// After prerender (link was Ready)
performance navigation -> activationStart fires, FCP ~ 0 ms

Browsers that do not support the API simply ignore the unknown script type, so this is safe to ship everywhere — it is pure progressive enhancement.

Step 2 — Choose prefetch or prerender deliberately

Reach for prefetch as your default. It is inexpensive, has almost no side effects, and still removes the slowest part of a navigation (the document download). Use prerender only for pages you are highly confident the user will visit next — the obvious “next step” in a flow, such as a product page from a listing, or step two of a checkout.

A prerender runs the target page’s JavaScript immediately, in the background. That means any analytics beacons, ad impressions, or fetch calls fire before the user has actually arrived. You must account for that (see Step 6). A prefetch has no such problem because scripts do not run until activation.

Concernprefetchprerender
Network savedDocument downloadDocument + all subresources
Runs page JS earlyNoYes
Cost to youLowHigh (full page load)
Best forLikely-but-uncertain linksNear-certain next page

Step 3 — Let the browser pick links with document rules

Listing URLs by hand does not scale. A document rule instead lets the browser consider any link on the page, filtered by a where condition. Here we prefetch every same-site link except logout and admin URLs:

<script type="speculationrules">
{
  "prefetch": [
    {
      "source": "document",
      "where": {
        "and": [
          { "href_matches": "/*" },
          { "not": { "href_matches": "/logout*" } },
          { "not": { "href_matches": "/admin/*" } },
          { "not": { "selector_matches": ".no-prefetch" } }
        ]
      },
      "eagerness": "moderate"
    }
  ]
}
</script>

href_matches uses URL pattern syntax, selector_matches uses CSS selectors, and and/not compose conditions. The rule above reads as: “consider all internal links, but never the logout link, anything under /admin, or any link I have tagged with the no-prefetch class.” That exclusion list is not optional — it is how you keep the browser away from links that do something when fetched.

Step 4 — Control aggression with eagerness

The eagerness field decides when the browser acts on a document rule. It is the single most important tuning knob, because it trades user-perceived speed against wasted work and bandwidth.

  • immediate — act as soon as the rule is seen. Maximum speed, maximum waste. Use only for a list of one or two near-certain URLs.
  • eager — act on the smallest hint of intent, well before hover. Aggressive.
  • moderate — act when the user hovers a link for ~200ms, or on pointer-down on touch. A strong default: by the time a finger or cursor commits, the page is loading.
  • conservative — act only on pointer-down (the instant before click). Lowest waste; still shaves off most of the latency.

A common production pattern is to mix tiers: prerender a single high-confidence URL with moderate eagerness, and prefetch everything else conservative. You can include multiple rule objects in the same script:

<script type="speculationrules">
{
  "prerender": [
    { "source": "document",
      "where": { "selector_matches": "a.primary-cta" },
      "eagerness": "moderate" }
  ],
  "prefetch": [
    { "source": "document",
      "where": { "href_matches": "/*" },
      "eagerness": "conservative" }
  ]
}
</script>

Step 5 — Make analytics survive prerendering

This is the gotcha that bites everyone. Because a prerendered page runs its JavaScript in the hidden tab, a naive analytics snippet will record a pageview the moment the page is speculated — not when the user arrives. You end up over-counting visits to pages nobody opened.

Two browser primitives fix this. document.prerendering is true while the page is in the hidden state, and the prerenderingchange event fires at activation. Defer any side-effecting work until the page is actually shown:

function trackPageview() {
  // send your real analytics beacon here
  navigator.sendBeacon("/collect", location.pathname);
}

if (document.prerendering) {
  // We are loading speculatively. Wait for the real visit.
  document.addEventListener("prerenderingchange", trackPageview, { once: true });
} else {
  // Normal load, or already activated.
  trackPageview();
}

You can also read activationStart from the navigation timing entry to measure how long the page sat prerendered before the user arrived — useful for confirming your rules are firing early enough:

const nav = performance.getEntriesByType("navigation")[0];
if (nav && nav.activationStart > 0) {
  console.log(`Page was prerendered ${Math.round(nav.activationStart)}ms before click`);
}

Step 6 — Ship rules from the server with a header

Inline scripts are easy, but if you want one source of truth across many pages — or your Content-Security-Policy blocks inline scripts — serve the rules from an external file and point to it with a response header. Save the JSON as /rules.json and serve it with the MIME type application/speculationrules+json:

# rules.json
{
  "prefetch": [
    { "source": "document",
      "where": { "href_matches": "/*" },
      "eagerness": "moderate" }
  ]
}
# HTTP response header on your HTML pages
Speculation-Rules: "/rules.json"

# Nginx example
add_header Speculation-Rules '"/rules.json"';

# Express example
app.use((req, res, next) => {
  res.set("Speculation-Rules", '"/rules.json"');
  next();
});

If you rely on inline scripts and use a strict CSP, remember to allow them — the relevant directive is script-src with the inline-speculation-rules source keyword.

Common pitfalls and how to avoid them

  • Prerendering destructive links. Never let the browser speculate URLs that mutate state on GET — logout, “add to cart”, “mark as read”, one-click unsubscribe. Exclude them with not rules. The correct long-term fix is to make those actions POST requests, which are never speculated.
  • Double-counted analytics and ad impressions. Always guard side-effecting scripts with document.prerendering as in Step 5. This applies to ad networks and A/B test bucketing too.
  • Wasting mobile data. immediate and eager on a broad document rule can prefetch dozens of pages a user never visits. Prefer moderate or conservative; the browser also self-limits and respects the user’s Data Saver setting, but do not lean on that.
  • Server load. Every prerender is a real request that runs your full page. A popular page with eager rules can multiply your traffic. Watch your logs after rolling out, and exclude expensive endpoints.
  • Assuming cross-site works like same-site. Cross-site prerendering is restricted and will not carry credentials in the same way; same-origin is where this API shines. Keep speculation to your own origin unless you have tested the cross-site case.
  • Forgetting graceful degradation testing. Because unsupported browsers ignore the rules, it is easy to ship a rule that silently does nothing in Chrome too (a typo in the JSON disables the whole block). Verify in DevTools → Application → Speculative loads.

Quick reference

Field / valueMeaning
source: "list"Speculate the explicit URLs in the urls array
source: "document"Let the browser pick links matching where
action: prefetchDownload the document only; cheap, no JS run
action: prerenderFully load + render in a hidden tab; instant swap
eagerness: immediateAct the moment the rule is parsed
eagerness: eagerAct on the earliest hint of intent
eagerness: moderateAct on ~200ms hover / pointer-down (good default)
eagerness: conservativeAct only on pointer-down (lowest waste)
where.href_matchesURL pattern filter for candidate links
where.selector_matchesCSS selector filter for candidate links
document.prerenderingtrue while the page loads speculatively
prerenderingchange eventFires when a prerendered page is activated

Next steps

Start small and safe: add a single prefetch document rule with moderate eagerness and an exclusion list for your destructive links. Ship it, watch DevTools and your server logs for a few days, and confirm your analytics still counts visits correctly. Once you trust it, promote your one or two most-clicked “next” links to prerender and feel the navigation become truly instant.

From there, explore the Chrome guide to implementing speculation rules for complex sites, measure the impact on your Largest Contentful Paint and Interaction to Next Paint, and consider centralizing your rules behind the Speculation-Rules header so every template benefits at once. The whole feature is progressive enhancement — you can adopt it incrementally with zero risk to browsers that do not support it yet.

Comments

Subscribe to join the conversation...

Be the first to comment