Prefetching Wasm for the Next Page

This page answers one task: a site has a page that needs a large WebAssembly module — an editor, a viewer, a calculator — and users usually reach it from another page. On arrival, they wait for the module to download. You want it downloaded (or even compiled) before they navigate, without wasting bandwidth for users who never go there.

Prerequisites

  • [ ] A multi-page site or an app with route-based code splitting.
  • [ ] The module served from a stable, cacheable URL with long-lived cache headers.
  • [ ] Analytics or a reasonable guess about which pages users go to next.

Prefetch, preload and prerender

Browsers offer several hints with different meanings. Preload (<link rel="preload">) fetches a resource the current page will need, at high priority. Prefetch (<link rel="prefetch">) fetches a resource a future navigation will probably need, at low priority, storing it in the HTTP cache. Speculation Rules (a JSON script type supported in Chromium) let a page declare URLs to prefetch — the document — or prerender — load and run the whole next page in the background — based on rules such as “links the user hovers”. For a Wasm module needed by the next page, prefetching the .wasm file puts it in the HTTP cache; prerendering the next page goes further, downloading, compiling and instantiating everything before the click.

Prefetching the module versus prerendering the next page Prefetching the .wasm file downloads it at low priority into the HTTP cache, so the next page skips the download but still compiles and instantiates. Prerendering the next page with Speculation Rules loads and runs it in the background, including compilation and instantiation, at a much higher resource cost. prefetch the .wasm download only low priority, cheap next page still compiles likely navigations prerender the next page download + compile + run near-instant navigation costly if not visited very likely navigations

Step 1 — prefetch the module on likely navigation pages

On a page that usually leads to the editor, add a prefetch hint for its module:

<link rel="prefetch" href="/assets/editor.7c41aa.wasm" as="fetch" type="application/wasm" crossorigin>

The browser downloads it when idle and stores it in the HTTP cache. When the editor page later calls instantiateStreaming(fetch(...)) for the same URL, the response comes from the cache. The crossorigin and as attributes must match how the module will be requested, or the cached response may not be reused (a mismatch is a common reason prefetches appear in the network log but are fetched again).

Step 2 — prefetch on intent rather than always

Prefetching on every page view downloads megabytes many users never need. Trigger it on stronger signals: hovering or focusing the link to the editor, the link entering the viewport, or after the user has spent some seconds on a page that commonly precedes the editor.

const editorLink = document.querySelector('a[href="/editor"]');
let prefetched = false;
function prefetchEditor() {
  if (prefetched) return;
  prefetched = true;
  const l = Object.assign(document.createElement("link"), { rel: "prefetch", href: EDITOR_WASM_URL, as: "fetch", crossOrigin: "anonymous" });
  document.head.append(l);
}
editorLink.addEventListener("pointerenter", prefetchEditor, { once: true });
editorLink.addEventListener("focus", prefetchEditor, { once: true });

Respect data-saver preferences: skip speculative downloads when navigator.connection?.saveData is true.

Step 3 — use Speculation Rules for whole-page prefetch or prerender

In Chromium, Speculation Rules can prerender the editor page when the user hovers its link, which runs the page’s scripts — including fetching and compiling the module — in the background:

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

moderate eagerness typically triggers on hover. Prerendering costs memory and CPU, so reserve it for navigations users take with high probability, and make sure the page behaves correctly when prerendered (defer analytics and side effects until activation using document.prerendering and the prerenderingchange event).

From hover to an instant editor The user hovers the link to the editor. Speculation rules start prerendering the editor page, which fetches the module, compiles it and instantiates it in the background. When the user clicks, the prerendered page is activated immediately with the module ready. user hovers link moderate eagerness prerender starts background page fetch + compile .wasm in prerendered page user clicks activation editor ready instantly no wait

Step 4 — make prefetches reusable with cache headers

A prefetched module is only useful if the HTTP cache keeps it and the next request can use it. Serve modules with content-hashed URLs and Cache-Control: public, max-age=31536000, immutable. Short max-ages or no-store waste prefetches; cache-busting query strings make the next page request a different URL entirely.

Step 5 — measure the benefit

Measure time from navigation start to “editor ready” for users who arrived with and without a prefetch (record whether the module response came from cache via Resource Timing). Also track prefetch waste: how many prefetches were followed by a navigation to the editor. A good rule is to keep prefetching where at least a substantial share of prefetches are used, and tighten triggers where most are wasted.

Single-page apps

In client-side routed apps there is no navigation to prefetch for, but the same idea applies: when the user hovers a link to a route that needs a module, start fetching and compiling it with WebAssembly.compileStreaming and keep the promise; the route then awaits a module that is already compiled or nearly so. See lazy loading Wasm on first use.

Prefetching compiled modules for in-app routes

Within a single-page application, prefetching can go further than the HTTP cache: start WebAssembly.compileStreaming for the next route’s module when intent appears, and keep the resulting promise in a module-level map. When the route mounts, it awaits that promise — often already resolved — and instantiates immediately. Compilation then happens during the user’s hover or idle time instead of after the click. Because compiling occupies background threads and memory, apply the same intent rules as for downloads, and cancel nothing — compiled modules are cheap to keep once done. If several routes share a module, the map ensures it is compiled once regardless of which route the user opens first. This pattern pairs well with workers: post the compiled module to the route’s worker as soon as both exist, so instantiation in the worker starts in parallel with the route’s rendering.

Cross-origin modules and prefetch

When modules are served from a CDN on another origin, prefetch and the later request must agree on credentials mode and CORS. Use crossorigin="anonymous" on the prefetch link and fetch the module without credentials (the default for fetch across origins is same-origin credentials, so cross-origin fetches send none), and make sure the CDN sends Access-Control-Allow-Origin for .wasm files. Pages that are cross-origin isolated also need the CDN to send Cross-Origin-Resource-Policy: cross-origin, or the module is blocked after being prefetched. Testing the prefetch path in an isolated page catches this, since the prefetch itself may succeed while the later use fails.

Caches are partitioned

Modern browsers partition the HTTP cache by top-level site, so a module prefetched while users browse another site cannot be reused on yours. Prefetching helps only for navigations within your own site, which is where it is meant to be used anyway.

Choosing triggers from data

Pick prefetch triggers from navigation data rather than guesses: which pages precede visits to the Wasm page most often, and how long users stay before navigating. Prefetch on those pages after a short dwell time, and on hover elsewhere.

Expected output

Hovering the “Open editor” link prefetches the 2.4 MB module; 68% of prefetches are followed by a visit to the editor; arriving users with a prefetched module reach “editor ready” in 420 ms instead of 1,900 ms on a mid-range connection; Chromium users with prerender see the editor ready on activation; and data-saver users receive no speculative downloads.

Gotchas

  • Prefetching on every page. Wasted bandwidth. Prefetch on intent.
  • Mismatched crossorigin or as. The module is fetched twice. Match the real request.
  • Short cache lifetimes. Prefetches expire. Use immutable, hashed URLs.
  • Side effects in prerendered pages. Analytics fire for pages never seen. Wait for activation.
  • Ignoring data saver. Respect user preferences.
  • Missing CORP on CDN modules for isolated pages. Prefetch succeeds, use fails. Send the header.

Performance note

On a 10 Mbit/s connection, arriving at the editor with the module prefetched cut time to ready from 1.9 s to 0.42 s; prerendering cut it to about 0.05 s after activation.

Time to editor ready after navigation Seconds from navigation start to the editor being ready without prefetch, with the module prefetched on hover, and with the whole page prerendered on hover, on a 10 Mbit/s connection. seconds to ready no prefetch 1.9 s module prefetched on hover 0.4 s page prerendered on hover 0.1 s

Frequently Asked Questions

Does prefetch compile the module? No — it only downloads into the HTTP cache. Prerendering or an explicit compileStreaming compiles.

Is Speculation Rules supported everywhere? It is a Chromium feature; other browsers ignore the rules, so keep rel=prefetch as a fallback.

Can a service worker do the prefetching? Yes — it can fetch and cache the module, and serve it to the next page.

How large a module is worth prefetching? Any module whose download noticeably delays the next page; weigh against waste for users who do not navigate.

Can an app prefetch a compiled module rather than just bytes? Yes — start compileStreaming on intent and keep the promise; the route awaits an already compiled module.

Does a prefetch from another site help mine? No — HTTP caches are partitioned by top-level site, so prefetching helps navigations within your own site.

What headers does a cross-origin module need for prefetch to work? CORS headers for the fetch, and Cross-Origin-Resource-Policy: cross-origin if the page is cross-origin isolated.

Should several routes share one compile promise? Yes — keep one promise per module URL so it is compiled once, whichever route opens first.

How do I decide which pages should prefetch? Use navigation data to find pages that most often precede the Wasm page, and prefetch there after a short dwell time.

← Back to Module Caching & Startup Performance