Tools
Tools: Speculation Rules API: Make Your Pages Load Before the User Clicks
2026-02-10
0 views
admin
The Problem ## The Solution: Cheat (the good kind) ## Prefetch vs Prerender ## How to Use It ## Option 1: Specific URLs ## Option 2: Automatic Rules (document rules) ## Eagerness: How Anxious Do You Want It to Be ## Useful Filters ## By URL pattern ## By CSS selector ## Exclude pages ## Combine conditions ## Add It Dynamically with JS ## Check Browser Support ## Detect If a Page Was Prerendered ## Things to Keep in Mind ## Browser Limits ## Resource Consumption ## Content Can Go Stale ## Extensions ## Deferred APIs ## Debug in DevTools ## Browser Support ## Now Here's the Plot Twist ## The Loop ## How to Build It ## The Eagerness Trick ## Measure the Impact ## Summary Imagine your website could predict where the user is going and have that page ready before they click. That's exactly what the Speculation Rules API does. It's not magic. It's not a framework. It's a native browser API. And it's stupidly easy to implement. When a user clicks a link, the browser has to: That takes time. Sometimes a little, sometimes a lot. But it always feels slower than it should. The Speculation Rules API tells the browser: "Hey, the user will probably go to this page. Get it ready now." And the browser does it. In the background. Without the user knowing. When they finally click, the page shows up instantly. Literally. 0ms of perceived wait time. There are two levels: Prefetch: Downloads only the HTML of the page. Like downloading the blueprints of a house but not building it. Prerender: Downloads EVERYTHING and renders the full page in the background. Like building the entire house and having it ready when you arrive. Prerender is more aggressive and uses more resources, but the experience is instant. It's a <script> tag with type="speculationrules" and JSON inside. No NPM, no imports, no config files. If you know exactly which pages you want to pre-load: This tells the browser: "Prerender /about, /work and /contact immediately." This is where it gets interesting. Instead of listing URLs by hand, you tell the browser to decide based on the links it finds on the page: Translation: "Prefetch all internal links on the page, except PDFs and links with the .no-prefetch class, but only when the user starts clicking." Controls when the browser starts pre-loading: conservative is the safest to start with. Only pre-loads when the user is already clicking, so you don't waste resources. My recommendation if you're unsure. moderate is the sweet spot. 200ms of hover is enough to have the page ready by the time the click lands. immediate is for when you're certain the user will go there. Use it with specific URLs, not document rules (or you'll prerender everything). Only links starting with /work/. Only links with that class. Important: Always exclude routes with side effects. If you prerender /logout, the user gets logged out without clicking. Not kidding. If you need to add rules after the page loads: Useful if you want to prerender the "next page" based on some user data. Useful if you want to defer analytics or other actions until the user actually sees the page. Chrome caps how many pages you can pre-load at once: Don't go crazy prerendering 100 pages. The browser will just ignore them. Prerender uses bandwidth, CPU and battery. Chrome automatically disables it if: If you prerender a page and the user takes 5 minutes to click, the content might have changed. For pages with real-time data, use prefetch instead of prerender. uBlock Origin disables preloading by default. Keep that in mind when measuring impact. Some APIs (Geolocation, Notifications, Storage) are delayed until the page is actually activated. They won't fire during prerender. For Firefox and Safari, the <script type="speculationrules"> tag is simply ignored. It doesn't break anything. Pure progressive enhancement. Everything above is cool. But if you hardcode your speculation rules and forget about them, you're leaving performance on the table. The real power of this API is that it's just JSON. And JSON can be generated. Dynamically. From data. What data? Your analytics. Think about it. Your analytics already know: So instead of guessing which pages to prerender, you can know. Week 1: You deploy speculation rules based on gut feeling. Prerender /about and /work because they seem important. Week 2: You check analytics. Turns out 73% of homepage visitors go to /work first, then /work/project-x. Nobody clicks /about from the homepage. Now you know what to actually prerender. Week 3: Traffic patterns shifted. A blog post went viral and now /play is getting 5x the traffic. Your speculation rules should reflect that. This isn't a "set it and forget it" feature. It's a feedback loop. The simplest version: a script that runs weekly (cron job, CI pipeline, whatever) that: Then in your template/layout: Here's where it gets smart. Use analytics to decide eagerness too: You're not wasting resources prerendering pages nobody visits. And you're aggressively prerendering the ones everybody does. Once you have this loop running, track: Feed that data back into the loop. Drop pages with low hit rates. Promote pages with high navigation probability. Every week your speculation rules get smarter. Static speculation rules are a quick win. Analytics-driven speculation rules are a compounding advantage. Every week your site gets faster because it gets smarter about what to pre-load. It's free, it's easy, and it makes a real difference. There's no reason not to use it. If this helped, drop a like and follow for more web performance content. Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse CODE_BLOCK:
<script type="speculationrules">
{ "prerender": [ { "urls": ["/about", "/work", "/contact"] } ]
}
</script> Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
<script type="speculationrules">
{ "prerender": [ { "urls": ["/about", "/work", "/contact"] } ]
}
</script> CODE_BLOCK:
<script type="speculationrules">
{ "prerender": [ { "urls": ["/about", "/work", "/contact"] } ]
}
</script> CODE_BLOCK:
<script type="speculationrules">
{ "prefetch": [ { "source": "document", "where": { "and": [ { "href_matches": "/*" }, { "not": { "href_matches": "*.pdf" } }, { "not": { "selector_matches": ".no-prefetch" } } ] }, "eagerness": "conservative" } ]
}
</script> Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
<script type="speculationrules">
{ "prefetch": [ { "source": "document", "where": { "and": [ { "href_matches": "/*" }, { "not": { "href_matches": "*.pdf" } }, { "not": { "selector_matches": ".no-prefetch" } } ] }, "eagerness": "conservative" } ]
}
</script> CODE_BLOCK:
<script type="speculationrules">
{ "prefetch": [ { "source": "document", "where": { "and": [ { "href_matches": "/*" }, { "not": { "href_matches": "*.pdf" } }, { "not": { "selector_matches": ".no-prefetch" } } ] }, "eagerness": "conservative" } ]
}
</script> CODE_BLOCK:
{ "href_matches": "/work/*" } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
{ "href_matches": "/work/*" } CODE_BLOCK:
{ "href_matches": "/work/*" } CODE_BLOCK:
{ "selector_matches": ".prerender-this" } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
{ "selector_matches": ".prerender-this" } CODE_BLOCK:
{ "selector_matches": ".prerender-this" } CODE_BLOCK:
{ "not": { "href_matches": "/logout" } } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
{ "not": { "href_matches": "/logout" } } CODE_BLOCK:
{ "not": { "href_matches": "/logout" } } CODE_BLOCK:
{ "and": [ { "href_matches": "/*" }, { "not": { "href_matches": "/api/*" } }, { "not": { "selector_matches": "a[rel~='nofollow']" } } ]
} Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
{ "and": [ { "href_matches": "/*" }, { "not": { "href_matches": "/api/*" } }, { "not": { "selector_matches": "a[rel~='nofollow']" } } ]
} CODE_BLOCK:
{ "and": [ { "href_matches": "/*" }, { "not": { "href_matches": "/api/*" } }, { "not": { "selector_matches": "a[rel~='nofollow']" } } ]
} CODE_BLOCK:
const rules = { prerender: [ { urls: ["/next-page"] } ]
}; const script = document.createElement("script");
script.type = "speculationrules";
script.textContent = JSON.stringify(rules);
document.body.append(script); Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
const rules = { prerender: [ { urls: ["/next-page"] } ]
}; const script = document.createElement("script");
script.type = "speculationrules";
script.textContent = JSON.stringify(rules);
document.body.append(script); CODE_BLOCK:
const rules = { prerender: [ { urls: ["/next-page"] } ]
}; const script = document.createElement("script");
script.type = "speculationrules";
script.textContent = JSON.stringify(rules);
document.body.append(script); CODE_BLOCK:
if ( HTMLScriptElement.supports && HTMLScriptElement.supports("speculationrules")
) { console.log("Speculation Rules supported");
} Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
if ( HTMLScriptElement.supports && HTMLScriptElement.supports("speculationrules")
) { console.log("Speculation Rules supported");
} CODE_BLOCK:
if ( HTMLScriptElement.supports && HTMLScriptElement.supports("speculationrules")
) { console.log("Speculation Rules supported");
} COMMAND_BLOCK:
if (document.prerendering) { console.log("This page is being prerendered");
} document.addEventListener("prerenderingchange", () => { console.log("User just navigated to this prerendered page");
}); Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
if (document.prerendering) { console.log("This page is being prerendered");
} document.addEventListener("prerenderingchange", () => { console.log("User just navigated to this prerendered page");
}); COMMAND_BLOCK:
if (document.prerendering) { console.log("This page is being prerendered");
} document.addEventListener("prerenderingchange", () => { console.log("User just navigated to this prerendered page");
}); COMMAND_BLOCK:
// build-speculation-rules.js
// Run this weekly via CI/cron async function generateRules() { // 1. Fetch top navigation paths from your analytics const topPaths = await getTopPathsFromAnalytics(); // e.g., [{ from: "/", to: "/work", percentage: 73 }, ...] // 2. Build rules per page const rulesPerPage = {}; for (const path of topPaths) { if (!rulesPerPage[path.from]) { rulesPerPage[path.from] = []; } rulesPerPage[path.from].push({ url: path.to, eagerness: path.percentage > 60 ? "moderate" : "conservative" }); } // 3. Write the output return rulesPerPage;
} Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
// build-speculation-rules.js
// Run this weekly via CI/cron async function generateRules() { // 1. Fetch top navigation paths from your analytics const topPaths = await getTopPathsFromAnalytics(); // e.g., [{ from: "/", to: "/work", percentage: 73 }, ...] // 2. Build rules per page const rulesPerPage = {}; for (const path of topPaths) { if (!rulesPerPage[path.from]) { rulesPerPage[path.from] = []; } rulesPerPage[path.from].push({ url: path.to, eagerness: path.percentage > 60 ? "moderate" : "conservative" }); } // 3. Write the output return rulesPerPage;
} COMMAND_BLOCK:
// build-speculation-rules.js
// Run this weekly via CI/cron async function generateRules() { // 1. Fetch top navigation paths from your analytics const topPaths = await getTopPathsFromAnalytics(); // e.g., [{ from: "/", to: "/work", percentage: 73 }, ...] // 2. Build rules per page const rulesPerPage = {}; for (const path of topPaths) { if (!rulesPerPage[path.from]) { rulesPerPage[path.from] = []; } rulesPerPage[path.from].push({ url: path.to, eagerness: path.percentage > 60 ? "moderate" : "conservative" }); } // 3. Write the output return rulesPerPage;
} COMMAND_BLOCK:
// Get the precomputed rules for the current page
const currentPageRules = speculationData[currentPath] || []; const rules = { prerender: currentPageRules .filter(r => r.eagerness === "moderate") .map(r => ({ urls: [r.url], eagerness: "moderate" })), prefetch: currentPageRules .filter(r => r.eagerness === "conservative") .map(r => ({ urls: [r.url], eagerness: "conservative" }))
}; Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
// Get the precomputed rules for the current page
const currentPageRules = speculationData[currentPath] || []; const rules = { prerender: currentPageRules .filter(r => r.eagerness === "moderate") .map(r => ({ urls: [r.url], eagerness: "moderate" })), prefetch: currentPageRules .filter(r => r.eagerness === "conservative") .map(r => ({ urls: [r.url], eagerness: "conservative" }))
}; COMMAND_BLOCK:
// Get the precomputed rules for the current page
const currentPageRules = speculationData[currentPath] || []; const rules = { prerender: currentPageRules .filter(r => r.eagerness === "moderate") .map(r => ({ urls: [r.url], eagerness: "moderate" })), prefetch: currentPageRules .filter(r => r.eagerness === "conservative") .map(r => ({ urls: [r.url], eagerness: "conservative" }))
}; COMMAND_BLOCK:
const navEntry = performance.getEntriesByType("navigation")[0]; if (navEntry.activationStart > 0) { // This page was prerendered! console.log("Prerender saved:", navEntry.activationStart, "ms");
} Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
const navEntry = performance.getEntriesByType("navigation")[0]; if (navEntry.activationStart > 0) { // This page was prerendered! console.log("Prerender saved:", navEntry.activationStart, "ms");
} COMMAND_BLOCK:
const navEntry = performance.getEntriesByType("navigation")[0]; if (navEntry.activationStart > 0) { // This page was prerendered! console.log("Prerender saved:", navEntry.activationStart, "ms");
} - Request the HTML from the server
- Download CSS, JS, images
- Parse everything
- Render the page - immediate/eager: up to 50 prefetches, 10 prerenders
- moderate/conservative: up to 2 of each - The device is in power saver mode
- Battery is low - Open Chrome DevTools
- Go to Application > Background Services > Speculative Loads
- Reload the page
- You'll see which pages are being prerendered/prefetched and any errors - Chrome: Yes (since 2024)
- Firefox: No - Which pages users visit most
- What the most common navigation paths are
- Which links get the most clicks on each page
- How those patterns change over time - Pulls your top navigation paths from Google Analytics, Plausible, or whatever you use
- Generates a JSON with the speculation rules
- Deploys it as a static file or injects it at build time - More than 60% of users navigate there? Use moderate (prerender on hover)
- Between 20-60%? Use conservative (prerender on mousedown)
- Less than 20%? Don't bother - LCP (Largest Contentful Paint) for pages that were prerendered vs not
- Navigation timing using the Performance API
- Hit rate: how often a prerendered page was actually visited - Add a <script type="speculationrules"> to your HTML
- Define which pages to pre-load and with what eagerness
- Your pages load instantly
- No libraries, no frameworks, no weird stuff
- Browsers that don't support it just ignore it
- Connect it to your analytics and update weekly -- that's where the real value is
how-totutorialguidedev.toaimlservercron