Guide

Speculation Rules API explained

Harbor Commerce's two-step checkout looked fast on paper — 420 KB total transfer, sub-second TTFB — but field Largest Contentful Paint on the shipping step averaged 1.4 seconds because users paid the full navigation cost the moment they clicked “Continue.” Product detail pages from the catalog were worse: every click to a sibling SKU re-parsed HTML, re-executed analytics, and re-hydrated React before paint. The team had already tuned resource hints for hero images; what they needed was whole-document speculation — fetching and optionally rendering the next page before the click. The Speculation Rules API delivers that as a declarative JSON rule list embedded in (or injected into) the current document. Browsers match links against your patterns, then prefetch or prerender targets at a chosen eagerness without per-link JavaScript. This guide explains prefetch vs prerender, matcher syntax, eagerness and privacy trade-offs, a Harbor Commerce checkout refactor, a technique decision table against hints and framework prefetch, common pitfalls, and a production checklist alongside our View Transitions and Performance Observer guides.

What document speculation adds beyond resource hints

Classic <link rel="prefetch"> downloads a resource at low priority for a future navigation. It does not parse HTML, run scripts, or build a render tree. rel="prerender" went further in older Chrome experiments but was removed from the declarative link surface. The Speculation Rules API unifies both behind a single rule object the browser evaluates centrally.

Prefetch vs prerender

Prefetch fetches the full navigation response (HTML and subresources the parser discovers) and stores it in the HTTP cache. The next navigation reuses cached bytes — faster than cold, but JavaScript still executes on click. Prerender loads the document in a hidden browsing context, runs scripts, applies styles, and may complete first paint. Activation swaps the prerendered page into view, often feeling instant. Prerender costs more CPU, memory, and bandwidth; reserve it for high-confidence next steps (checkout continue, obvious next article).

A third mode, preload (in speculation rules, not to be confused with rel="preload" for assets), fetches without caching for reuse — rarely needed for navigations. Most production configs use prefetch for exploratory links and prerender for one or two funnel-critical URLs.

Rule syntax: sources, targets, and eagerness

Rules live in a <script type="speculationrules"> block containing JSON. The top-level object holds arrays keyed by mode: prefetch, prerender, or preload.

{
  "prerender": [{
    "source": "document",
    "where": {
      "href_matches": "/checkout/shipping*"
    },
    "eagerness": "moderate"
  }],
  "prefetch": [{
    "source": "list",
    "urls": ["/catalog/widget-pro", "/catalog/widget-lite"],
    "eagerness": "conservative"
  }]
}

Source types

  • document — scan same-origin (or permitted) anchor href values in the current page and speculate on matches.
  • list — explicit URL list; useful for funnel steps with no visible link yet.
  • href (with urls) — single known target, often injected after an A/B assignment.

Where matchers

The where object filters candidates. href_matches accepts URL patterns with * wildcards (path-prefix style, not full regex). selector_matches limits to links inside a CSS selector (e.g. ".product-grid a"). relative_to can scope to document or list context. Combine matchers to avoid speculating login or logout URLs.

Eagerness levels

Conservative speculates only on pointer down — minimal wasted work, still helps slow networks. Moderate (default) starts on pointer hover or touch start; good for desktop catalog grids. Eager speculates as soon as rules are parsed; use sparingly on landing pages with one obvious CTA. Immediate bypasses heuristics for critical list rules (checkout step two). Browsers may still throttle on Save-Data, low battery, or memory pressure — rules are hints, not guarantees.

Privacy, analytics, and security guardrails

Prerender executes third-party tags in a hidden context. Without care, page-view beacons fire twice — once at prerender, once at activation. Modern browsers expose document.prerendering (and the legacy PerformanceNavigationTiming activation start) so analytics SDKs can defer counts until the page is shown. Harbor Commerce wrapped their RUM snippet: if document.prerendering, queue events until prerenderingchange.

Cross-origin prerender requires target pages to opt in via Supports-Loading-Mode: credentialed-prerender (for authenticated flows) or default anonymous prerender for public catalog pages. Never prerender pages that mutate state on load (empty cart, one-time tokens) unless the hidden context cannot commit side effects — treat prerender as a full navigation dry run. Content Security Policy and cookie SameSite rules still apply; speculation does not bypass auth.

expects_no_vary_search

For URLs whose response varies only on certain query keys, set "expects_no_vary_search": true when query strings are cosmetic (UTM params) so the browser can reuse a single prerendered instance. Mis-setting this on personalized pricing pages shows stale prices — validate with Vary headers first.

Harbor Commerce refactor: checkout and catalog

Problem. Step-one cart → step-two shipping had 62% click-through but 18% abandonment before LCP on mobile. Traces showed 900 ms script evaluation dominating, not network.

Change. They added a speculation rules block on the cart page:

{
  "prerender": [{
    "source": "list",
    "urls": ["/checkout/shipping"],
    "eagerness": "immediate"
  }],
  "prefetch": [{
    "source": "document",
    "where": {
      "selector_matches": ".related-skus a",
      "href_matches": "/catalog/*"
    },
    "eagerness": "moderate"
  }]
}

Shipping prerender ran once per cart session (guarded by a sessionStorage flag so cart edits invalidated stale prerender). Related SKU links prefetched only — lower risk if the user never clicks. Analytics deferred via prerenderingchange; ad slots stayed empty until activation to avoid policy issues.

Result. Median LCP on shipping dropped from 1.4 s to 180 ms on prerender-supported Chrome; prefetch-only Safari still gained ~400 ms from warm cache. Combined with route-level code splitting on the shipping bundle, INP on the continue button improved 35%. They paired activation with startViewTransition for a cross-fade — polish, not load time.

Technique decision table

Approach What it speculates Best for Limitations
Speculation Rules (prefetch) Full document + discovered assets Same-origin link grids, related content Chrome/Edge 121+; no full prerender on all engines
Speculation Rules (prerender) Hidden browsing context to first paint Checkout funnels, single obvious next page Analytics/state risk; memory cap per tab
link rel=prefetch One URL, cache only Legacy fallback, single known URL No matcher DSL; deprecated prerender link removed
Framework prefetch (Next.js, etc.) Router-aware JS/data payloads SPA route segments, RSC flight data Framework-specific; not full HTML prerender
Service Worker precache Shell or static assets Offline PWAs, app shell Does not prerender arbitrary server HTML per user
Hover JS fetch() Manual, per handler Custom auth headers, GraphQL POST Maintenance burden; easy to over-fetch

Common pitfalls

  • Double-counting analytics — prerender fires tags early; gate on document.prerendering or use browser-supported activation metrics.
  • Prerendering stateful pages — cart checkout that reserves inventory on load can corrupt orders; prerender idempotent pages only.
  • Eager rules on dense pages — fifty prerender candidates exhaust memory; cap with tight href_matches or use conservative eagerness.
  • Ignoring Save-Data — test with Data Saver on; browsers downgrade or skip speculation — do not assume always-on speedups.
  • Stale personalized content — wrong expects_no_vary_search shows cached prices or A/B variants.
  • Competing with critical LCP assets — immediate prerender on landing pages can steal bandwidth from the hero image; measure with Performance Observer.
  • No invalidation on mutation — cart quantity change after prerender requires discarding the hidden context (reload rules or navigate fresh).
  • Cross-origin without opt-in — external doc links silently skip prerender; prefetch may still work for anonymous GET.

Production checklist

  • Identify top 1–3 high-confidence next URLs from funnel analytics.
  • Choose prefetch (exploratory) vs prerender (funnel-critical) per URL class.
  • Write href_matches / selector_matches; exclude auth and logout paths.
  • Set eagerness: immediate for single funnel step, moderate for grids, conservative on mobile-heavy traffic.
  • Defer analytics and ads until activation; test prerenderingchange handlers.
  • Verify Vary and personalization headers before expects_no_vary_search.
  • Measure LCP/INP before and after with field RUM; segment by browser support.
  • Provide fallback: no rules block should break navigation on unsupported browsers.
  • Invalidate prerender after client-side state changes (cart, form drafts).
  • Document bandwidth budget; monitor prerender abort rates in Chrome internals.

Key takeaways

  • Speculation Rules declare whole-document prefetch and prerender in JSON — matchers and eagerness replace per-link JavaScript.
  • Prerender hides first-paint cost for high-confidence navigations; prefetch warms cache with lower risk.
  • Analytics must respect document.prerendering or activation events to avoid double counting.
  • Harbor Commerce cut shipping-step LCP from 1.4s to ~180ms with immediate prerender plus deferred RUM.
  • Pair speculation with resource hints for assets and View Transitions for perceived speed — measure on real devices with Save-Data scenarios.

Related reading