Speculation Rules in Next.js: How We Prerender Hard Navigations Without Breaking Analytics or CSP
Table of Contents
About the Author

Ahmed Mahmoud
Author & Developer
Software engineer passionate about web development and user experience design.
Founder of Devya · eng-ahmed.com ↗Headline: The Speculation Rules API lets Chromium-based browsers prefetch or fully prerender a likely next page before the user clicks. In a Next.js App Router app it only speeds up hard navigations, so we scope the rules to those links, use moderate eagerness, exclude every URL with side effects, and delay analytics until the prerendered page is activated.
Key takeaways
• The Speculation Rules API is a Chrome and Edge feature that prefetches the next document or prerenders it in a hidden tab, configured with a script element of type speculationrules or a Speculation-Rules header.
• Next.js Link prefetching and speculation rules solve different problems: Link speeds up client-side navigations, while speculation rules speed up only full document navigations.
• At Devya we scope document rules to hard-navigation links, because a catch-all /* rule also matches every Next.js Link anchor and triggers server renders that the client-side router discards.
• Moderate eagerness is our default: Chrome speculates after a 200 ms hover or on pointerdown, and keeps at most 2 such speculations at a time.
• Prerendered pages run JavaScript before the user sees them, so page-view analytics must wait until document.prerendering is false, and side-effecting URLs must be excluded.
What is the Speculation Rules API?
The Speculation Rules API is a web platform API that lets a page declare which URLs the browser may load before a navigation happens. It has two actions. Prefetch downloads only the HTML of the next document, without subresources or JavaScript execution. Prerender loads and renders the entire page in an invisible background tab, including subresources and scripts, and swaps it in when the user clicks.
Rules come in two forms. List rules name exact URLs. Document rules match links in the current page with a where condition: href_matches for URL patterns and selector_matches for CSS selectors, combined with and, or, and not. The eagerness field decides when Chrome acts.
• immediate: speculates as soon as the rules are observed; Chrome allows up to 50 prefetches and 10 prerenders.
• eager: on desktop, from Chrome 143, a pointer resting on a link for about 10 ms; on mobile, shortly after the link enters the viewport; the same 50 and 10 limits apply.
• moderate: on desktop, a 200 ms hover or pointerdown; on mobile, viewport heuristics; at most 2 at a time, with the oldest evicted first.
• conservative: pointerdown or touchstart; at most 2 at a time, with the oldest evicted first.
List rules default to immediate and document rules default to conservative. Chrome and Edge ship the API, Safari has it behind a flag, and Firefox does not support it. Unsupported browsers ignore the rules entirely, which makes speculation rules a progressive enhancement that needs no fallback code.
Does Next.js Link prefetching make speculation rules redundant?
No. The Next.js Link component and the Speculation Rules API accelerate different kinds of navigation. A Link click inside the same root layout is a soft navigation: the App Router fetches a React Server Component (RSC) payload and updates the page through history.pushState, so the browser never requests a new document. A prerendered document cannot help that click. Speculation rules only help when the browser performs a full document navigation.
• Next.js Link prefetch: fetches the RSC payload, speeds up soft navigations, and works in every browser.
• Speculation rules prefetch: fetches only the HTML document and speeds up hard navigations in Chromium-based browsers.
• Speculation rules prerender: loads the full page, runs its JavaScript before activation, and speeds up hard navigations in Chromium-based browsers.
In the Next.js projects our team builds, hard navigations come from four places: plain anchors inside CMS or markdown HTML that the App Router does not intercept; links into a separate Next.js application on the same domain through multi-zones, such as a docs app under /docs; links between two different root layouts, which the App Router turns into a full page load; and links from a static landing page into the Next.js app.
The common trap is a catch-all rule. A document rule with href_matches set to /* matches every anchor on the page, and the Next.js Link component renders a real anchor element. With moderate eagerness, every hover prerenders a full document on the server, and the client-side router then performs a soft navigation that discards the prerender. That is one server render per hover with no benefit to the user.
How do we add speculation rules to a Next.js App Router app?
The simplest approach is an inline script element with type speculationrules, rendered from the root layout. We keep the rules in one module and scope them to the links that really cause hard navigations: a prerender rule with moderate eagerness for paths under a separate zone such as /docs/*, excluding anything marked with a data-no-prerender attribute, and a conservative prefetch rule for anchors that opt in with a data-hard-nav attribute.
A Content Security Policy (CSP) blocks inline speculation rules unless script-src allows them. There are three options: a nonce, a hash of the JSON, or the 'inline-speculation-rules' keyword, which permits speculation rules JSON without permitting arbitrary inline JavaScript. On our nonce-based pages, the layout reads the nonce from the request headers set by the same middleware that writes the CSP header, so the policy stays strict.
For statically rendered pages we prefer the Speculation-Rules HTTP response header. The header points to a JSON file that the browser fetches separately, so the layout never needs a per-request nonce. We serve that JSON from a static Next.js route handler with the application/speculationrules+json content type, because the browser ignores rule files served with any other type, and we add the header to every path through the headers() function in next.config.ts. Inline rules and header rules can coexist; the browser merges every rule set it finds.
What breaks when the browser prerenders a page?
A prerendered page executes its JavaScript before the user decides to visit it, so any code that assumes loaded means viewed becomes wrong. We review four areas before enabling prerender on a client project.
Side-effecting GET URLs. Logout, one-click unsubscribe, magic-link sign-in, and mark-as-read URLs must never be speculated, because a prefetch or prerender is a real GET request. We exclude them with not conditions or a data-no-prerender attribute.
Analytics. A page-view event that fires on load counts prerenders that the user never saw. The fix is to wait for activation: document.prerendering is true while the page is prerendering, and the prerenderingchange event fires when the user actually navigates to it. Our page-view helper checks document.prerendering and, when it is true, registers a one-time prerenderingchange listener before sending the event. Some analytics libraries already handle this, so we verify the vendor behaviour before adding our own guard.
Server-side work. Speculative requests carry a Sec-Purpose request header: prefetch for prefetches and prefetch;prerender for prerenders. We read it in middleware to tag speculative requests in logs and metrics. The header is supplied by the client, so we never use it to bypass rate limits or authorization.
Cross-origin links. Chrome prerenders only same-origin URLs by default; a same-site subdomain such as app.example.com must opt in with the Supports-Loading-Mode: credentialed-prerender response header. Next.js multi-zones on one domain avoid that, because every zone shares one origin.
How do we verify that speculation rules are working?
Chrome DevTools shows every speculation under Application, then Background services, then Speculative loads: parsed rule sets, prefetched and prerendered URLs, and the failure reason for each cancelled speculation. We check that panel after every rule change, because an invalid rule set fails silently.
In real-user monitoring, activationStart in the Navigation Timing entry is greater than zero when a page was prerendered and then activated. The web-vitals library reports a navigationType of prerender for those navigations and measures Largest Contentful Paint (LCP) from activation. We segment LCP by navigation type so the gain is not averaged away by browsers that ignore the rules.
Cost matters as much as speed. We compare requests carrying Sec-Purpose: prefetch;prerender with activations; a large gap means the rules are too eager or too broad, so we move from moderate to conservative eagerness or narrow the href_matches patterns before changing anything else.
FAQ
Q: Do speculation rules work in Safari and Firefox?
A: Not by default. Chrome and Edge support the Speculation Rules API, Safari has it behind a feature flag, and Firefox does not support it. Unsupported browsers ignore the speculationrules script, so adding it never breaks them.
Q: Is prerender or prefetch the safer default?
A: Prefetch is safer because it downloads only the HTML document and runs no JavaScript. Prerender makes activation faster but executes the page early, so analytics and side-effect handling must be fixed first.
Q: Can speculation rules hurt performance?
A: Yes, when the rules are too broad. Eager rules on link-heavy pages waste bandwidth, server capacity, and device memory, which is why Chrome caps concurrent speculations and why we start with moderate eagerness on a narrow set of URLs.
Further Reading
Frontend Engineering
JSON-LD in the Next.js App Router: How We Type, Serialize, and Test Structured Data — Field Notes
Our team renders JSON-LD from Next.js App Router Server Components, escapes every less-than sign so content can't close the script tag, types the graph with schema-dts, and keeps it in sync with the page through a CI test. Here's which schema.org types still earn their place in 2026.
Frontend Engineering
Shipping Feature Flags in Next.js with the Vercel Flags SDK
We moved a client's ad-hoc environment-variable flags onto the Flags SDK and the precompute pattern. Here's what the migration looked like and why static rendering was the part worth protecting.