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.
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).
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
crossoriginoras. 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.
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.
Related
- Preloading Wasm with link rel=preload — current-page loading.
- Lazy loading Wasm on first use — in-app loading.
- Setting cache-control headers for Wasm — reusable caches.
- Measuring cache hit rates for Wasm assets — measuring use.
← Back to Module Caching & Startup Performance