Medium wave · 540–1600 kHz79 stn
1502kHz

Prerendering Runs Your Page Before Anyone Arrives

A dark steampunk theater where a brass clockwork stage has already raised its curtain and lit every glowing neon footlight and spinning gear for a performance, while the rows of velvet seats sit completely empty in drifting fog

You add a dozen lines of JSON to your layout, hover over a link, click it, and the next page appears with no load at all. Not fast. Instant. The HTML was fetched, parsed, styled, and laid out, and its scripts ran, before you finished deciding to click.

Then the analytics dashboard shows page views up 30% with bounce rate climbing. The “most read” widget has a new favorite. Somebody’s A/B test assignment no longer matches what they saw.

Nothing is broken. Your page ran for an audience that hadn’t arrived yet, and most front-end code was written on the assumption that it never would.


What prerender actually does

Chromium’s Speculation Rules API lets a page declare which links are likely next and how aggressively to act on them:

<script type="speculationrules">
  {
    "prerender": [
      {
        "where": { "href_matches": "/*" },
        "eagerness": "moderate"
      }
    ]
  }
</script>

There are two actions. prefetch downloads the next document and stops there. prerender goes the whole way: it loads the page in an invisible tab, fetches its subresources, and executes its JavaScript. When the user clicks, the browser swaps that tab in, and that swap is why it feels like zero milliseconds.

The eagerness setting controls when this starts. conservative waits for pointerdown or touchstart, moderate fires on a short hover on desktop, and eager and immediate act sooner on less evidence. The more eager the setting, the more pages get prerendered that nobody ever sees.

For a static site, the server side is essentially free. The next page is a CDN cache hit. The cost and the risk live entirely in the browser, in whatever your page does on load.


Everything keyed to “load” is now keyed to “maybe”

The bug class is simple to state: code that runs at load and assumes load means a human is looking at the page. On a static site that is usually third-party or near-third-party code.

  • Analytics. A page-view beacon fired at script execution counts a view whether or not the prerender is ever shown. Some vendors already handle prerendering correctly. Check yours rather than assuming.
  • View counters and “popular posts.” A static site’s one bit of server state is often a hit counter behind a serverless function. Prerender inflates it in exact proportion to how hoverable your links are.
  • Experiment assignment. If the bucketing script logs an exposure event at load, users get counted as exposed to variants they never saw.
  • Time-on-page and scroll timers. The clock starts at execution, not display. A prerender that sits for 20 seconds before the click reports 20 seconds of engagement that never happened.
  • Ad and embed impressions. An impression that loaded but was never visible is the kind of thing that ends up in a billing dispute.
  • State snapshots. A script that reads localStorage once at load, like a theme preference, captured the value at prerender time. If the user changes it on the current page and then clicks, the new page can arrive with the old setting.

None of this is exotic. It is just code that never had to distinguish “this document exists” from “someone is looking at this document.”


The fix is one property and one event

The platform gives you exactly what you need. document.prerendering is true while the page is running in the hidden tab, and prerenderingchange fires on document at the moment it is activated:

function whenActivated(callback) {
  if (document.prerendering) {
    document.addEventListener('prerenderingchange', callback, { once: true });
  } else {
    callback();
  }
}

whenActivated(() => {
  sendPageView();
  startEngagementTimer();
  applyStoredTheme();
});

Wrap anything with a side effect in that helper. Pure rendering work like building the table of contents or hydrating a component can still run early. That’s the whole point of prerendering. What has to wait is anything that reports, counts, assigns, or reads state that might change before the click.

On the server, prerender requests carry a Sec-Purpose header that includes prerender. A static host won’t let you act on it, but if you run a counter endpoint, that header is how you tell speculative hits apart in the logs.

A useful audit: search your layout for every script that sends a network request at load. Each one is either harmless to run early or needs whenActivated. There is no third category.


Prerender only fetches with GET, and GET is supposed to be safe. Real sites break that rule more often than they admit: /logout, /unsubscribe?token=…, /cart/add?id=…, /api/track-click?…. A static marketing site rarely owns those, but it often shares an origin with something that does.

Opt them out explicitly instead of hoping nobody hovers:

<script type="speculationrules">
  {
    "prerender": [
      {
        "where": {
          "and": [
            { "href_matches": "/*" },
            { "not": { "href_matches": "/logout" } },
            { "not": { "href_matches": "/api/*" } },
            { "not": { "selector_matches": "[rel~=nofollow], .no-prerender" } }
          ]
        },
        "eagerness": "moderate"
      }
    ]
  }
</script>

The .no-prerender class is a cheap escape hatch for one-off links in content. It is also documentation. Anyone reading the markup can see that link was considered and excluded on purpose.


Pick the eagerness you can defend

The tempting configuration is eager on every internal link. Resist it.

moderate is the right default for content sites. A short hover is a strong signal on desktop, and the page is ready by the time the click lands. conservative is the right choice for heavy pages, the ones with large client-side bundles or multiple third-party embeds, because pointerdown still buys you a meaningful head start without paying for pages nobody opens.

Reserve eager for a genuinely dominant next step, like “Next chapter” in a docs sequence or the single call to action on a landing page. Chromium caps how many prerenders it keeps alive and backs off under data-saver and low-memory conditions anyway, so blanket eagerness mostly burns your visitors’ bandwidth for nothing.

Framework support is landing too. Astro’s prefetch can be set to use Speculation Rules where the browser supports them, and other static tools are adding similar switches. Turning it on is the easy part. The audit above is the actual work, whether the rules come from a framework flag or a script tag you wrote yourself.


Verify it, then trust it

Chrome DevTools has a Speculative loads panel under Application that lists every rule set, every URL it matched, and whether each prerender succeeded or why it was canceled. Open it, hover across your nav, and watch.

Then test the side effects directly:

  1. Open the network panel with “Preserve log” on.
  2. Hover a link long enough to trigger a prerender, then move away without clicking.
  3. Confirm that no analytics, counter, or exposure request was sent.
  4. Click the link and confirm those requests fire then, exactly once.

If step 3 shows a beacon, you have a counting bug, and the dashboard has been lying since the day you shipped the rules.


The takeaway

Prerendering is the closest thing the web has to a free lunch for static sites. The HTML is already cached at the edge, and the navigation becomes instant. The price is one assumption you have to give up: a script running is not the same as a person watching.

Gate every side effect behind document.prerendering, exclude the links that change state, choose eagerness on purpose, and verify with a hover-and-leave test. Do that, and instant navigation costs you twelve lines of JSON. Skip it, and it costs you every metric you thought you could trust.