MinhVo

Minh Vo

rss feed

Slaying code & making it lit fr fr 🔥 tagline

Hey there 👋 I'm an AI Engineer with 7 years of experience building scalable web and mobile applications. Currently at Neurond AI (May 2025 — present), architecting an Enterprise AI Assistant Platform with multi-tenant RAG on pgvector, multi-provider LLM orchestration, and Azure-native infrastructure. Previously spent 5+ years at SNAPTEC (Sep 2019 — Apr 2025), leading SaaS themes, admin dashboards, and e-commerce platforms — earned the Hero of the Year award in 2021. I specialize in TypeScript, React, Next.js, and AI-Native engineering with Claude Code and Cursor.bio

Back to blogs

Speculative Loading: Speculation Rules API

Pre-render and prefetch pages with the Speculation Rules API for instant navigation.

PerformanceSpeculation RulesFrontendBrowser

By MinhVo

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.

Browser performance optimization

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
DeliveryHTML link tagHTML link tagJSON (script tag or HTTP header)
ScopeSingle URLSingle URLPattern-based rules
ConditionNoneNoneURL patterns, eagerness levels
Pre-renderNoLimited (deprecated)Full pre-render
Browser supportBroad (partial)Chrome only (deprecated)Chrome 121+
IntegrationNoneNoneNavigation 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:

LevelTriggerUse Case
immediateOn page loadCritical next pages (rare, aggressive)
eagerOn pointer-down or hoverHigh-confidence navigations
moderateOn hover for 200ms+Most interactive links
conservativeOn 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 support
  • selector_matches — CSS selector for matching anchor elements
  • and, 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

  1. 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.

  2. 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.

  3. Use eagerness levels strategically: moderate (200ms hover) is a good default. Use eager only for navigation elements with very high click-through rates. Use conservative for expensive-to-render pages.

  4. Exclude sensitive pages: Never prerender pages that perform side effects (checkout, payment, logout). Only speculate on idempotent GET requests.

  5. Measure with RUM: Use Real User Monitoring to track prerender hit rates. If users prerender pages they never visit, you're wasting resources.

  6. 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.

  7. Account for stale prerenders: Prerendered pages may show stale data if the user waits before clicking. Use the prerenderingchange event to refresh critical data on activation.

  8. Respect user data preferences: On slow connections or data-saving modes, reduce speculation aggressiveness. The navigator.connection.saveData API helps detect these conditions.

Common Pitfalls and Solutions

PitfallImpactSolution
Prerendering pages with side effectsDuplicate operations, unintended state changesOnly prerender idempotent GET requests
Too many simultaneous prerendersMemory pressure, battery drainLimit to 2–3 concurrent prerenders
Not handling stale prerender dataUsers see outdated contentRefresh data on prerenderingchange event
Ignoring CSP configurationSpeculation rules silently blockedAdd CSP headers for speculation rules
Prerendering behind authenticationUnauthenticated page shown or errorsOnly prerender public pages
Not testing in non-Chrome browsersMissing fallback for Safari/FirefoxAlways 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

FeatureSpeculation Rules<link rel="prefetch">Service Worker CachingQuicklink (library)
Pre-renderYesNoNo (cache only)No
Pattern matchingYes (JSON rules)No (per-URL)Yes (fetch handler)Yes (intersection)
Browser controlYes (throttling, limits)MinimalManualManual
Resource costBrowser-managedUser-controlledUser-controlledUser-controlled
Bundle size0 (native)0 (native)SW script (~5KB)~1.5KB
Browser supportChrome 121+BroadBroad (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-description

Building 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.

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:

  1. Prefetch vs prerender: Prefetch downloads HTML; prerender downloads, parses, and renders the full page in a hidden context
  2. Eagerness levels control when speculations fire: from immediate (on load) to conservative (on click)
  3. Pattern matching enables rules that apply to URL patterns, not just individual URLs
  4. Browser-managed throttling prevents resource abuse better than manual prefetch libraries
  5. Graceful degradation makes it safe to adopt today — unsupported browsers ignore the rules
  6. Integration with Navigation API, service workers, and CSP requires careful configuration
  7. Measurement with activationStart enables 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.