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) anchorhrefvalues in the current page and speculate on matches.list— explicit URL list; useful for funnel steps with no visible link yet.href(withurls) — 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.prerenderingor 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_matchesor 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_searchshows 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
prerenderingchangehandlers. - Verify
Varyand personalization headers beforeexpects_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.prerenderingor 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
- Resource hints explained — preload, preconnect, and asset-level prefetch
- Core Web Vitals explained — LCP, INP, and CLS field metrics
- View Transitions API explained — morph activations after prerender
- Performance Observer API explained — validate speculation wins in RUM