Introduction
Every millisecond of page load time matters. Google's research shows that a 100ms delay in load time reduces conversion rates by 7%, and 53% of mobile users abandon sites that take longer than 3 seconds to load. The Speculation Rules API, shipped in Chrome 121, addresses this by allowing developers to tell the browser which pages users are likely to visit next, so the browser can fetch and even pre-render those pages before the user clicks.
This is not the first attempt at speculative loading. The <link rel="prefetch"> and <link rel="prerender"> hints have existed for years, but they operate on a per-resource basis and have limited browser support. The Speculation Rules API is different: it uses a JSON-based rule engine that supports conditional matching, eagerness levels, and both prefetch and full pre-render operations. It is designed for modern web applications with client-side routing, dynamic content, and complex navigation patterns.
Understanding the Speculation Rules API is essential for web performance optimization. Combined with the Navigation API, View Transitions, and service worker caching, it forms part of a modern performance toolkit that can deliver near-instant page transitions.
Understanding the Speculation Rules API
What It Does
The Speculation Rules API lets developers declare rules about which URLs the browser should prefetch (download the HTML) or pre-render (download, parse, and execute the page in a hidden tab). When the user navigates to a pre-rendered page, the browser swaps in the already-rendered content instantly.
How It Differs from Previous Hints
| Feature | <link rel="prefetch"> | <link rel="prerender"> | Speculation Rules API |
|---|---|---|---|
| Delivery | HTML link tag | HTML link tag | JSON (script tag or HTTP header) |
| Scope | Single URL | Single URL | Pattern-based rules |
| Condition | None | None | URL patterns, eagerness levels |
| Pre-render | No | Limited (deprecated) | Full pre-render |
| Browser support | Broad (partial) | Chrome only (deprecated) | Chrome 121+ |
| Integration | None | None | Navigation API, service workers |
Browser Support
The Speculation Rules API is available in:
- Chrome 121+ (stable, launched January 2024)
- Edge 121+ (Chromium-based)
- Opera 107+ (Chromium-based)
Safari and Firefox do not yet support it. The API degrades gracefully — browsers that don't support it ignore the <script type="speculationrules"> tag entirely. Always combine with traditional prefetch hints for cross-browser coverage.
Core Concepts
Speculation Actions
There are two actions:
Prefetch downloads the HTML of a target page but does not parse or execute JavaScript. This saves the network round-trip time (~100–500ms depending on latency). The page is stored in a prefetch cache and used when the user navigates.
Prerender goes further: it downloads, parses, executes JavaScript, and renders the page in a hidden context. When the user navigates, the fully-rendered page swaps in instantly. This costs more resources but delivers the best user experience.
<script type="speculationrules">
{
"prefetch": [
{
"urls": ["/about", "/products", "/contact"],
"eagerness": "moderate"
}
],
"prerender": [
{
"urls": ["/checkout"],
"eagerness": "conservative"
}
]
}
</script>Eagerness Levels
Eagerness controls when the browser acts on the speculation rule:
| Level | Trigger | Use Case |
|---|---|---|
immediate | On page load | Critical next pages (rare, aggressive) |
eager | On pointer-down or hover | High-confidence navigations |
moderate | On hover for 200ms+ | Most interactive links |
conservative | On click (before navigation) | Low-confidence speculations |
<script type="speculationrules">
{
"prerender": [
{
"where": { "selector_matches": ".high-priority-link" },
"eagerness": "eager"
},
{
"where": { "selector_matches": "a[href^='/blog/']" },
"eagerness": "moderate"
}
]
}
</script>URL Matching
Rules can specify URLs explicitly or use pattern matching:
<script type="speculationrules">
{
"prefetch": [
{
"where": {
"and": [
{ "href_matches": "/*" },
{ "not": { "href_matches": "/admin/*" } },
{ "not": { "href_matches": "/api/*" } }
]
},
"eagerness": "moderate"
}
]
}
</script>Supported matchers:
href_matches— URL pattern with wildcard supportselector_matches— CSS selector for matching anchor elementsand,or,not— boolean combinators
Step-by-Step Implementation
Basic Setup
Add a speculation rules script to your HTML:
<!DOCTYPE html>
<html>
<head>
<title>My Website</title>
<script type="speculationrules">
{
"prefetch": [
{
"where": { "href_matches": "/*" },
"eagerness": "moderate"
}
]
}
</script>
</head>
<body>
<!-- content -->
</body>
</html>Dynamic Rule Generation with JavaScript
For client-side routed applications, generate rules dynamically:
function injectSpeculationRules(urls: string[], action: "prefetch" | "prerender" = "prefetch") {
document.querySelectorAll('script[type="speculationrules"]').forEach((el) => el.remove());
const rules = {
[action]: [
{
urls,
eagerness: action === "prerender" ? "conservative" : "moderate",
},
],
};
const script = document.createElement("script");
script.type = "speculationrules";
script.textContent = JSON.stringify(rules);
document.head.appendChild(script);
}
function collectNavigationLinks(): string[] {
const links = document.querySelectorAll('a[href^="/"]');
const urls = new Set<string>();
links.forEach((link) => {
const href = (link as HTMLAnchorElement).pathname;
if (!href.startsWith("/api") && !href.startsWith("/admin")) {
urls.add(href);
}
});
return Array.from(urls).slice(0, 10);
}Integration with the Navigation API
The Navigation API provides intercepts that work well with speculation rules:
interface SpeculationRuleConfig {
maxPrefetch: number;
maxPrerender: number;
excludedPaths: string[];
}
class SpeculationManager {
private config: SpeculationRuleConfig;
private prefetched = new Set<string>();
private prerendered = new Set<string>();
constructor(config: SpeculationRuleConfig) {
this.config = config;
this.setupIntersectionObserver();
}
private isExcluded(url: string): boolean {
return this.config.excludedPaths.some((path) => url.startsWith(path));
}
private setupIntersectionObserver() {
const observer = new IntersectionObserver(
(entries) => {
const visibleLinks = entries
.filter((entry) => entry.isIntersecting)
.map((entry) => entry.target as HTMLAnchorElement)
.filter((link) => !this.isExcluded(link.pathname));
this.updateSpeculationRules(visibleLinks);
},
{ rootMargin: "200px" }
);
document.querySelectorAll('a[href^="/"]').forEach((link) => {
observer.observe(link);
});
}
private updateSpeculationRules(links: HTMLAnchorElement[]) {
const prefetchUrls = links
.map((l) => l.pathname)
.filter((url) => !this.prefetched.has(url))
.slice(0, this.config.maxPrefetch);
const prerenderUrls = links
.filter((l) => l.dataset.prerender === "true")
.map((l) => l.pathname)
.filter((url) => !this.prerendered.has(url))
.slice(0, this.config.maxPrerender);
if (prefetchUrls.length > 0) {
this.prefetched = new Set([...this.prefetched, ...prefetchUrls]);
injectSpeculationRules(prefetchUrls, "prefetch");
}
if (prerenderUrls.length > 0) {
this.prerendered = new Set([...this.prerendered, ...prerenderUrls]);
injectSpeculationRules(prerenderUrls, "prerender");
}
}
}
const speculationManager = new SpeculationManager({
maxPrefetch: 5,
maxPrerender: 2,
excludedPaths: ["/api", "/admin", "/auth"],
});Checking Prerender Status
When a page loads from prerender, you can detect it:
if (document.prerendering) {
console.log("Page is currently being prerendered");
}
document.addEventListener("prerenderingchange", () => {
console.log("Page finished prerendering and is now visible");
});
const navEntry = performance.getEntriesByType("navigation")[0] as PerformanceNavigationTiming;
if (navEntry?.activationStart) {
const prerenderDuration = navEntry.activationStart - navEntry.startTime;
console.log(`Prerender saved ${prerenderDuration}ms`);
}Advanced Patterns
Combining with Service Workers
Service workers can enhance speculation rules by serving prefetched content from cache:
// service-worker.ts
self.addEventListener("fetch", (event: FetchEvent) => {
const url = new URL(event.request.url);
if (event.preloadResponse) {
event.respondWith(
event.preloadResponse.then((response) => {
if (response) return response;
return fetch(event.request);
})
);
return;
}
if (event.request.mode === "navigate") {
event.respondWith(
fetch(event.request).catch(() => caches.match("/offline.html"))
);
}
});Framework Integration (Next.js)
Next.js 14+ integrates with the Speculation Rules API for prefetching route segments:
// next.config.js
module.exports = {
experimental: {
speculationRules: {
prefetch: [
{
source: "document",
where: { href_matches: "/*" },
eagerness: "moderate",
},
],
},
},
};Content Security Policy
If your site uses Content Security Policy, you may need to allow the speculation rules:
Content-Security-Policy: script-src 'self' 'unsafe-inline'; script-type application/speculationrules
Alternatively, use the HTTP header Speculation-Rules to serve rules from a URL:
Speculation-Rules: /speculation-rules.json
Real-World Use Cases
Use Case 1: E-Commerce Product Browsing
Users viewing a product listing often click multiple product pages. Prerendering the top visible products on hover delivers instant page transitions, reducing bounce rate and increasing conversion.
Use Case 2: Documentation Sites
Documentation sites have predictable navigation patterns — users click through linked pages sequentially. Prefetching the next and previous pages in the sidebar reduces perceived load time to near zero.
Use Case 3: News and Media Sites
Media sites benefit from prerendering article pages from the homepage. Users typically click the first or second headline, so prerendering the top 2–3 articles on page load is high-value.
Use Case 4: SaaS Application Navigation
SaaS dashboards with predictable navigation flows (dashboard → report → detail) can prerender the most common next step, making the application feel as responsive as a native app.
Best Practices for Production
-
Start with prefetch, upgrade to prerender selectively: Prefetch costs fewer resources and works well for most pages. Reserve prerender for high-confidence, high-value navigations.
-
Limit the number of speculations: Don't prerender every link on the page. Each prerender consumes memory and CPU. Limit to 2–3 prerenders and 5–10 prefetches.
-
Use eagerness levels strategically:
moderate(200ms hover) is a good default. Useeageronly for navigation elements with very high click-through rates. Useconservativefor expensive-to-render pages. -
Exclude sensitive pages: Never prerender pages that perform side effects (checkout, payment, logout). Only speculate on idempotent GET requests.
-
Measure with RUM: Use Real User Monitoring to track prerender hit rates. If users prerender pages they never visit, you're wasting resources.
-
Combine with resource hints: Use
<link rel="preconnect">for third-party domains that pages depend on. Speculation rules handle the HTML, but third-party scripts still need connection setup. -
Account for stale prerenders: Prerendered pages may show stale data if the user waits before clicking. Use the
prerenderingchangeevent to refresh critical data on activation. -
Respect user data preferences: On slow connections or data-saving modes, reduce speculation aggressiveness. The
navigator.connection.saveDataAPI helps detect these conditions.
Common Pitfalls and Solutions
| Pitfall | Impact | Solution |
|---|---|---|
| Prerendering pages with side effects | Duplicate operations, unintended state changes | Only prerender idempotent GET requests |
| Too many simultaneous prerenders | Memory pressure, battery drain | Limit to 2–3 concurrent prerenders |
| Not handling stale prerender data | Users see outdated content | Refresh data on prerenderingchange event |
| Ignoring CSP configuration | Speculation rules silently blocked | Add CSP headers for speculation rules |
| Prerendering behind authentication | Unauthenticated page shown or errors | Only prerender public pages |
| Not testing in non-Chrome browsers | Missing fallback for Safari/Firefox | Always include <link rel="prefetch"> fallback |
Performance Optimization
class AdaptiveSpeculation {
private connection: NetworkInformation | null;
constructor() {
this.connection = (navigator as any).connection || null;
}
shouldSpeculate(): boolean {
if (this.connection?.saveData) return false;
const effectiveType = this.connection?.effectiveType;
if (effectiveType === "2g" || effectiveType === "slow-2g") return false;
if ((navigator as any).deviceMemory && (navigator as any).deviceMemory < 4) return false;
return true;
}
getEagerness(): "immediate" | "eager" | "moderate" | "conservative" {
if (!this.connection) return "moderate";
switch (this.connection.effectiveType) {
case "4g": return "moderate";
case "3g": return "conservative";
default: return "conservative";
}
}
getMaxPrerender(): number {
if (!this.connection) return 2;
const memory = (navigator as any).deviceMemory || 4;
if (memory >= 8) return 3;
if (memory >= 4) return 2;
return 1;
}
}Measuring Impact
function measurePrerenderSavings() {
const entries = performance.getEntriesByType("navigation");
const nav = entries[0] as PerformanceNavigationTiming;
if (nav.activationStart > 0) {
const savings = nav.activationStart - nav.startTime;
console.log(`Prerender saved ${savings}ms of navigation time`);
navigator.sendBeacon("/api/analytics", JSON.stringify({
event: "prerender_savings",
duration: savings,
url: location.pathname,
}));
}
}
window.addEventListener("load", measurePrerenderSavings);Comparison with Alternatives
| Feature | Speculation Rules | <link rel="prefetch"> | Service Worker Caching | Quicklink (library) |
|---|---|---|---|---|
| Pre-render | Yes | No | No (cache only) | No |
| Pattern matching | Yes (JSON rules) | No (per-URL) | Yes (fetch handler) | Yes (intersection) |
| Browser control | Yes (throttling, limits) | Minimal | Manual | Manual |
| Resource cost | Browser-managed | User-controlled | User-controlled | User-controlled |
| Bundle size | 0 (native) | 0 (native) | SW script (~5KB) | ~1.5KB |
| Browser support | Chrome 121+ | Broad | Broad (with polyfill) | Broad |
Future Outlook
The Speculation Rules API is part of a broader set of Chrome performance primitives including the Navigation API, View Transitions, and Content Visibility. Together they enable near-instant page transitions that approach native app responsiveness.
Mozilla (Firefox) has expressed positive signals about implementing the Speculation Rules API, though no timeline has been announced. Safari has not yet indicated plans. The API's graceful degradation makes it safe to adopt today — unsupported browsers simply ignore the rules.
Expect to see framework-level integrations increase. Next.js, Nuxt, and Astro are all exploring built-in speculation rules configuration that analyzes route structures and generates optimal rules automatically.
Production Deployment and Operations
Running backend services in production requires attention to reliability, observability, and operational concerns that don't exist in development environments. Proper deployment practices ensure your service remains available and performant under real-world conditions.
Graceful Shutdown Handling
Implement graceful shutdown to prevent request failures during deployments and restarts:
const server = app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
async function gracefulShutdown(signal) {
console.log(`Received ${signal}, starting graceful shutdown...`);
// Stop accepting new connections
server.close(async () => {
console.log('HTTP server closed');
try {
// Wait for existing requests to complete (with timeout)
await Promise.race([
waitForActiveRequests(),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Shutdown timeout')), 30000)
),
]);
// Close database connections
await db.destroy();
await redis.quit();
console.log('Graceful shutdown completed');
process.exit(0);
} catch (error) {
console.error('Error during shutdown:', error);
process.exit(1);
}
});
// Force shutdown after timeout
setTimeout(() => {
console.error('Forced shutdown after timeout');
process.exit(1);
}, 35000);
}
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));Structured Logging
Replace console.log with structured logging that supports log aggregation and querying:
const pino = require('pino');
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
formatters: {
level(label) {
return { level: label };
},
},
serializers: {
err: pino.stdSerializers.err,
req: pino.stdSerializers.req,
res: pino.stdSerializers.res,
},
redact: {
paths: ['req.headers.authorization', 'req.headers.cookie'],
remove: true,
},
});
// Request logging middleware
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
logger.info({
req,
res,
responseTime: Date.now() - start,
}, `${req.method} ${req.url} ${res.statusCode}`);
});
next();
});Rate Limiting and Abuse Prevention
Protect your API endpoints with rate limiting that adapts to different client types:
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
const apiLimiter = rateLimit({
store: new RedisStore({ client: redisClient }),
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window
standardHeaders: true,
legacyHeaders: false,
keyGenerator: (req) => req.user?.id || req.ip,
handler: (req, res) => {
logger.warn({ ip: req.ip, user: req.user?.id }, 'Rate limit exceeded');
res.status(429).json({
error: 'Too many requests',
retryAfter: Math.ceil(req.rateLimit.resetTime / 1000),
});
},
});
app.use('/api/', apiLimiter);These operational practices form the foundation of a reliable production service that can handle real-world traffic patterns and failure scenarios.
Community Resources and Further Learning
The technology landscape evolves rapidly, making continuous learning essential for maintaining expertise. Building a systematic approach to staying current with developments in your technology stack ensures you can leverage new features and avoid deprecated patterns.
Curated Learning Pathways
Rather than consuming content randomly, create structured learning pathways aligned with your current projects and career goals. Start with official documentation and specification documents, which provide the most accurate and comprehensive information. Follow this with hands-on tutorials and workshops that reinforce concepts through practical application.
Technical blogs from framework maintainers and core team members often provide deeper insights into design decisions and upcoming features. Subscribe to the official blogs of your primary frameworks and libraries to stay ahead of breaking changes and deprecation timelines.
Contributing to Open Source
Contributing to open-source projects in your technology stack provides unparalleled learning opportunities. Start with documentation improvements and bug reports, then progress to fixing small issues tagged as "good first issue" in your favorite projects. This direct engagement with maintainers and the codebase accelerates your understanding far beyond what passive learning can achieve.
# Setting up for contribution
git clone https://github.com/project/repository.git
cd repository
git checkout -b fix/issue-description
# Run the project's contribution setup
npm run setup:dev
npm run test # Ensure tests pass before making changes
# Make your changes, then run the full test suite
npm run test:full
npm run lint
npm run build
# Submit your contribution
git add -A
git commit -m "fix: description of the fix
Closes #1234"
git push origin fix/issue-descriptionBuilding a Technical Knowledge Base
Maintain a personal knowledge base that captures insights, solutions, and patterns you discover during your work. Tools like Obsidian, Notion, or even a simple Markdown repository can serve as an external memory that grows more valuable over time.
Organize your notes by topic rather than chronologically, and include code examples, links to relevant documentation, and explanations of why certain approaches work better than others. When you encounter a particularly insightful article or conference talk, write a summary that captures the key takeaways and how they apply to your current projects.
Staying Current with Industry Trends
Follow key conferences and their published talks to stay informed about emerging patterns and best practices. Many conferences publish recorded talks on YouTube within weeks of the event, making world-class technical content freely accessible.
Join relevant Discord servers, Slack communities, and forums where practitioners discuss real-world challenges and solutions. These communities provide early warning about emerging issues and access to collective wisdom that isn't available through formal documentation.
Mentorship and Knowledge Sharing
Teaching others is one of the most effective ways to deepen your own understanding. Consider writing technical blog posts, giving talks at local meetups, or mentoring junior developers. The process of explaining concepts to others forces you to organize your knowledge and identify gaps in your understanding.
Pair programming sessions with colleagues of different experience levels create mutual learning opportunities. Senior developers gain fresh perspectives on problems they've solved the same way for years, while junior developers benefit from exposure to production-grade thinking and decision-making processes.
Conclusion
The Speculation Rules API is a significant advancement in web performance optimization:
- Prefetch vs prerender: Prefetch downloads HTML; prerender downloads, parses, and renders the full page in a hidden context
- Eagerness levels control when speculations fire: from
immediate(on load) toconservative(on click) - Pattern matching enables rules that apply to URL patterns, not just individual URLs
- Browser-managed throttling prevents resource abuse better than manual prefetch libraries
- Graceful degradation makes it safe to adopt today — unsupported browsers ignore the rules
- Integration with Navigation API, service workers, and CSP requires careful configuration
- Measurement with
activationStartenables tracking real user savings
Combined with modern CSS (content-visibility, will-change), efficient JavaScript bundling, and edge caching, the Speculation Rules API helps deliver the near-instant page transitions that users expect from modern web applications.