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.
Prerequisites
- [ ] A hashed
.wasmURL 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.
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.
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.
Related
- Module caching & startup performance — where this sits among the levers.
- Reducing Wasm cold-start latency — the ordered checklist this belongs to.
- Bundling Wasm ESM with Vite — generating the hashed URL the hint needs.
- Streaming vs ArrayBuffer instantiation — what happens once the bytes arrive.
← Back to Module Caching & Startup Performance