Vercel Logo

Navigation Performance

Cross-app navigation means full page loads. Speculation Rules make those navigations feel instant by prefetching before users click.

Outcome

Implement Speculation Rules in the shared Header to prefetch and prerender cross-app pages before users click.

The Problem: Hard Navigations

In a single-page app, clicking a link triggers a client-side route change. It's fast and seamless. In microfrontends:

Click "Docs" in marketing app
└── Full page load to docs app
    └── HTML request
    └── JavaScript download
    └── React hydration
    └── Visible content

This takes longer than a client-side transition. Users notice.

The Solution: Speculation Rules

Speculation Rules tell the browser to prefetch or prerender pages before users navigate:

  • Prefetch - Download the HTML and critical resources
  • Prerender - Fully render the page in a hidden tab

When the user clicks, the page is already ready.

Speculation Rules only work in Chrome 109+ and Edge 109+. Safari and Firefox ignore the script. No error, just no prefetch optimization for ~25% of users.

Fast Track

  1. Add Speculation Rules script to the shared Header
  2. Configure prefetch for moderate eagerness
  3. Configure prerender for conservative eagerness

Hands-on Exercise 3.1

Add Speculation Rules to the shared Header component.

Part 1: Create the Speculation Rules Component

Create packages/ui/src/speculation-rules.tsx:

packages/ui/src/speculation-rules.tsx
export function SpeculationRules() {
  const rules = {
    prefetch: [
      {
        source: "document",
        where: {
          and: [
            { href_matches: "/*" },
            { not: { href_matches: "/api/*" } },
            { not: { href_matches: "/_next/*" } },
          ],
        },
        eagerness: "moderate",
      },
    ],
    prerender: [
      {
        source: "document",
        where: {
          and: [
            { href_matches: "/*" },
            { not: { href_matches: "/api/*" } },
            { not: { href_matches: "/_next/*" } },
          ],
        },
        eagerness: "conservative",
      },
    ],
  };
 
  return (
    <script
      type="speculationrules"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(rules) }}
    />
  );
}

moderate eagerness prefetches on hover. conservative only prerenders when the click starts (mousedown).

Part 2: Add to Header

Update packages/ui/src/header.tsx:

packages/ui/src/header.tsx
import { SpeculationRules } from "./speculation-rules";
 
export function Header() {
  return (
    <>
      <SpeculationRules />
      <header className="h-16 border-b bg-white px-6 flex items-center justify-between">
        {/* ... existing header content ... */}
      </header>
    </>
  );
}

Part 3: Export the Component

Update packages/ui/src/index.ts:

packages/ui/src/index.ts
export { Header } from "./header";
export { Footer } from "./footer";
export { SpeculationRules } from "./speculation-rules";

Understanding Eagerness Levels

LevelWhen It TriggersUse Case
immediatePage loadCritical paths users always visit
eagerLink appears in viewportHigh-traffic links
moderateHover or focus on linkMost navigation links
conservativeClick starts (mousedown)Resource-heavy pages

For cross-app navigation:

  • Prefetch: moderate - Download resources when user shows intent
  • Prerender: conservative - Only fully render when click is certain

This balances performance gains against resource usage.

Try It

  1. Run pnpm dev
  2. Open DevTools → Network
  3. Visit localhost:3024 (Marketing)
  4. Hover over "Docs" in the header
  5. Watch the network tab - you should see prefetch requests for /docs
  6. Click "Docs" - the navigation should feel faster

Measuring the Improvement

Without Speculation Rules:

Click → DNS → Connection → HTML → JS → Hydration → Interactive
       ~50ms    ~50ms     ~100ms  ~200ms  ~150ms
       Total: ~550ms

With prefetch:

Hover → Prefetch (background)
Click → HTML (cached) → JS → Hydration → Interactive
        ~0ms          ~200ms  ~150ms
        Total: ~350ms (36% faster)

With prerender:

Click-start → Prerender (background)
Click-end → Swap (instant)
            Total: ~50ms (90% faster)

Commit

git add -A
git commit -m "perf: add Speculation Rules for cross-app prefetching"

Done-When

  • SpeculationRules component created
  • Header includes Speculation Rules script
  • Prefetch triggers on hover (moderate)
  • Network tab shows prefetch requests on hover

Advanced: Targeted Speculation

For high-traffic pages, increase eagerness:

const rules = {
  prefetch: [
    // High-traffic: prefetch eagerly
    {
      source: "document",
      where: { href_matches: ["/docs", "/pricing"] },
      eagerness: "eager",
    },
    // Everything else: prefetch on hover
    {
      source: "document",
      where: { href_matches: "/*" },
      eagerness: "moderate",
    },
  ],
};

What's Next

Write tests that catch routing misconfigurations before they reach production.