Preloading Wasm with link rel=preload

This guide shows how to start fetching a .wasm binary while the browser is still parsing HTML, instead of waiting until the JavaScript that imports it has downloaded, parsed and executed — and how to decide whether that is a good idea for your page.

The mechanism is a single <link> tag, but three details have to be right or the hint is either ignored or actively harmful: the as value, the type, and the crossorigin attribute. Getting one wrong typically produces a second, duplicate download rather than an error.

What the hint moves earlier Normally the binary is only discovered when the module that imports it executes, so its download starts after HTML, CSS and JavaScript have been fetched and parsed. A preload hint lets the parser start the request immediately, overlapping it with the rest of the load. without a preload HTML JS bundle execute .wasm download starts here with a preload hint HTML JS bundle .wasm downloads in parallel already in the cache when the module asks for it The saving is the length of the JavaScript bar — larger on slow connections and on pages with heavy bundles.

Prerequisites

  • [ ] A hashed .wasm URL you can emit from the build rather than hard-code
  • [ ] A module that is genuinely needed early — otherwise this hurts
  • [ ] The ability to add tags to the document head
  • [ ] DevTools’ Network panel, to confirm one request rather than two

Step-by-step procedure

1. Add the hint with all three attributes

<link rel="preload"
      href="/wasm/kernel-4b7e0d.wasm"
      as="fetch"
      type="application/wasm"
      crossorigin>

as="fetch" is correct because the binary is retrieved with fetch, not by a script tag. type lets the browser skip the hint if it would not accept the resource. crossorigin is required even for same-origin requests here, because fetch uses CORS mode by default and a hint fetched in a different mode will not be reused.

2. Generate it from the build

Hard-coding the hashed filename guarantees it goes stale. Emit it alongside the asset:

// vite.config.ts — plugin sketch
transformIndexHtml(html, ctx) {
  const wasm = Object.keys(ctx.bundle ?? {}).find((f) => f.endsWith(".wasm"));
  if (!wasm) return html;
  const tag = `<link rel="preload" href="/${wasm}" as="fetch" type="application/wasm" crossorigin>`;
  return html.replace("</head>", `${tag}</head>`);
}

3. Confirm the preload is reused, not duplicated

Network panel, filtered to Wasm:
kernel-4b7e0d.wasm   preload   200   74.9 kB
kernel-4b7e0d.wasm   fetch     200   (from prefetch cache)

Two rows where the second says “from prefetch cache” is success. Two rows with two real downloads means the attributes do not match the actual request.

4. Watch the console for the unused warning

The resource /wasm/kernel-4b7e0d.wasm was preloaded using link preload
but not used within a few seconds from the window's load event.

That warning means you preloaded something the page did not need promptly — remove the hint or defer the resource properly instead.

5. Prefer modulepreload for the glue

If the loader is an ES module, its own preload directive is modulepreload, and most bundlers emit it for you:

<link rel="modulepreload" href="/assets/kernel-9f1c2a.js">

Expected output

On a throttled connection, the same page with and without the hint:

without preload
  wasm request starts   at 640 ms   (after the bundle executed)
  module ready          at 910 ms

with preload
  wasm request starts   at  90 ms   (during HTML parsing)
  module ready          at 470 ms

The saving is roughly the time the browser spent fetching and executing JavaScript before it discovered the binary. On a fast connection with a small bundle that is tens of milliseconds; on a slow connection with a large bundle it can be several hundred.

When the hint helps and when it costs For a module the first screen depends on, preloading removes discovery latency at no cost. For a module behind a rarely-used feature, the hint competes for bandwidth with resources the visitor actually needs and is usually a net loss. preload this the first screen needs it or the first interaction certainly will discovery latency removed, nothing wasted do not preload this a feature most visitors never open a module behind a route change competes with what the page does need A preload is a promise that the resource is needed now. Breaking that promise costs other requests their bandwidth.

Gotchas

The resource downloads twice. The attributes do not match the eventual request — most often a missing crossorigin. Both requests appear in the Network panel, which makes this easy to spot and easy to miss if you are not looking.

The hint is ignored entirely. as is wrong. as="fetch" for a binary retrieved with fetch; as="script" is for a script tag and will not match.

The URL is stale after a deploy. The tag was hard-coded with a hash that changed. Generate it.

The console warns that the preload was unused. The page did not need the module promptly. That is the hint telling you it was the wrong resource to preload.

Preloading everything makes the page slower. Hints compete for the same connection. Two or three genuinely critical resources is the useful range; a dozen is counterproductive.

Three attributes, all required The as value must describe how the resource will be fetched, the type lets the browser skip an unsupported resource, and crossorigin must match because fetch uses CORS mode. A mismatch on any of them results in the resource being downloaded twice. as="fetch" must match how it is retrieved wrong value → hint ignored type="application/wasm" lets the browser skip it if unsupported cheap insurance crossorigin required even same-origin omit it → two downloads The double-download failure is silent apart from the Network panel, which is why checking for "from prefetch cache" is worth doing once.

Performance note

Preloading changes when a download starts, not how long it takes. Its value is therefore proportional to how late the resource would otherwise be discovered — which is why it pays most on pages with a large JavaScript bundle in front of the module and hardly at all on a page where the loader is the first script.

It also interacts with the rest of the checklist rather than replacing it. A preloaded 400 KB binary still takes 400 KB of time to arrive; the hint just starts the clock sooner. Shrink first, preload second, and confirm both with the same measurement.

Priority, and competing with other resources

A preload hint does not only start a download earlier — it also raises that request’s priority, which means it competes with everything else the page needs. On a fast connection that is invisible. On a slow one, preloading a large binary can delay the CSS and fonts that decide when the page renders at all, producing a page that is technically faster to interactive and visibly slower to appear.

The rule that follows is to preload at most the resources on the critical path, and to order them by what the user sees first. A page rendering meaningful content without the module should let stylesheet and font requests go first; the module can start a moment later and still be ready before anyone can interact. A page that renders nothing until the module runs is a different case, and there the module genuinely is critical — though that architecture is worth questioning independently, because it makes every user wait for the slowest asset.

fetchpriority gives finer control where the ordering matters:

<link rel="preload" href="/wasm/kernel-4b7e0d.wasm" as="fetch"
      type="application/wasm" crossorigin fetchpriority="low">

Low priority still starts the request during parsing — capturing most of the benefit — while letting render-blocking resources go first. For a module needed on interaction rather than on paint, that is usually the right combination.

Checking the hint is doing anything

Because a badly configured preload is silent, verify it once per deployment target rather than trusting it. Three checks cover it:

1. Network panel, filtered to Wasm — exactly two rows, the second "from prefetch cache"
2. Console — no "preloaded but not used" warning after load
3. The request's Initiator column — the preload row, not the JavaScript module

If the second row is a real download rather than a cache hit, the attributes do not match. If the warning appears, the resource was not needed promptly and the hint is costing bandwidth. If the initiator is the module rather than the preload, the hint was ignored entirely — usually a wrong as value.

Thirty seconds of checking, once, is what separates a preload that saves several hundred milliseconds from one that quietly downloads the binary twice.

Frequently Asked Questions

Should I preload the glue as well as the binary? Usually the bundler already emits modulepreload for the JavaScript. Adding both is fine when both are critical, and pointless when the glue is part of the main bundle.

Does preloading interact with a compiled-module cache? Only on the first visit. Once the module is cached in IndexedDB, no fetch happens at all and the hint would be wasted — which is an argument for emitting it conditionally if your first-visit and repeat-visit paths differ that much.

What about prefetch instead? prefetch is a low-priority hint for a future navigation, and it is the right tag for a module the next page will need. preload is for the current page.

Can a preload hint be added dynamically from JavaScript? It can — creating a <link rel="preload"> element and appending it works — but by then the parser has already moved on, so most of the benefit is gone. The value of the hint is that it is visible during HTML parsing, which means emitting it in the markup rather than adding it later.

Does it help on a repeat visit? Only if the resource is not already cached. When it is, the browser serves from cache and the hint is harmless but pointless. That asymmetry is another reason to reserve preloading for resources on the first-visit critical path.

Should I preload from a service worker instead? A service worker can warm its cache in the background, which serves a different purpose: it helps the next visit rather than this one. Preload is for the current navigation; a service worker’s warming is for the one after it, and a page that cares about both will use each for its own job.

← Back to Module Caching & Startup Performance