Streaming Instantiation vs ArrayBuffer Instantiation
This guide answers one question precisely: when should you load a module with
WebAssembly.instantiateStreaming(fetch(url)) versus the older fetch → arrayBuffer() →
WebAssembly.instantiate(bytes) path, and how do you write a single function that prefers the fast path
but never breaks when it is unavailable?
Prerequisites
- [ ] A modern engine (Chrome/Edge 119+, Firefox 120+, Safari 17+) exposing
WebAssembly.instantiateStreaming. - [ ] A built
.wasmartifact reachable over HTTP from your page. - [ ] A server (or dev proxy) you can configure to send response headers, so you can set the MIME type.
- [ ] Familiarity with the broader Wasm instantiation lifecycle — fetch, compile, instantiate.
The two paths are not competing equals; streaming is the default you reach for and ArrayBuffer is the
escape hatch for the cases streaming cannot handle — an uncontrollable Content-Type, bytes already in
memory, or an environment without the streaming API. The goal of this guide is to make that decision
mechanical: prefer streaming, detect when it cannot work, and fall back without doubling your network cost.
Why streaming is faster
WebAssembly.instantiateStreaming accepts a Response (or a promise of one) and compiles the module’s
bytes as they download. The engine’s streaming parser validates and emits machine code from each chunk
the moment it arrives, so by the time the final byte lands, most compilation is already done. The
ArrayBuffer path cannot overlap: response.arrayBuffer() must resolve the entire download first, and
only then does WebAssembly.instantiate(bytes) begin compiling. You pay download time and compile time
serially instead of in parallel.
The catch is a hard requirement: streaming reads the HTTP response’s Content-Type and refuses to
compile anything that is not exactly application/wasm. This is a spec requirement, not a browser quirk —
streaming bytes off the wire is only safe if the server has declared them to be a Wasm module. Serve the
file with the wrong type and instantiateStreaming throws a TypeError.
There is a second, subtler advantage to streaming: lower peak memory. The ArrayBuffer path materializes the
entire compressed-then-decompressed binary as one contiguous ArrayBuffer in JavaScript before compilation
starts, so for a brief window you hold both the full byte buffer and the engine’s compiled output. Streaming
consumes chunks as they arrive and never needs the whole binary resident in a JavaScript-visible buffer,
which matters on memory-constrained devices and for very large modules. The download is also not blocked
behind a single arrayBuffer() promise, so the engine can begin useful work at the first chunk rather than
the last.
Compile vs instantiate
Both APIs come in a “compile only” and an “instantiate” flavor. compileStreaming and compile produce a
reusable WebAssembly.Module; instantiateStreaming and instantiate go one step further and bind an
import object to produce a WebAssembly.Instance. When you only need the module once, instantiate
directly. When you will create many instances or cache across sessions, compile once and instantiate the
cached Module repeatedly.
The reason to care about the split is cost. Compilation is the expensive, CPU-bound phase — tens of
milliseconds for a multi-megabyte binary — and its output, the WebAssembly.Module, is stateless and
reusable. Instantiation is cheap: it allocates a fresh linear memory, wires up imports, and runs the
start function, typically in well under a millisecond. So the choice between the “instantiate” and
“compile-only” flavors is really a choice about reuse. One module instantiated once? Use an instantiate
call. One module instantiated many times — a worker pool, a hot path, a return visit? Compile once and pay
the cheap instantiate cost per use.
Step-by-step procedure
1. The streaming path (preferred)
const importObject = { env: { /* imports your module declares */ } };
const { module, instance } = await WebAssembly.instantiateStreaming(
fetch("/app.wasm"),
importObject,
);
instance.exports.main();
2. The ArrayBuffer path (compatible)
Use this when you cannot control the response’s Content-Type, when the bytes already live in memory (a
bundler inlined them, or you decoded them yourself), or for environments without streaming support.
const response = await fetch("/app.wasm");
const bytes = await response.arrayBuffer(); // wait for the full download
const { module, instance } = await WebAssembly.instantiate(bytes, importObject);
instance.exports.main();
3. A robust fallback that tries streaming first
This helper prefers streaming, catches the MIME/streaming failure, and falls back to the ArrayBuffer path — without fetching twice on the happy path.
async function loadWasm(url, importObject = {}) {
if (typeof WebAssembly.instantiateStreaming === "function") {
try {
return await WebAssembly.instantiateStreaming(fetch(url), importObject);
} catch (err) {
// Wrong Content-Type or no streaming support: fall through, do NOT rethrow.
console.warn("Streaming instantiation failed, falling back:", err.message);
}
}
const response = await fetch(url);
const bytes = await response.arrayBuffer();
return WebAssembly.instantiate(bytes, importObject);
}
const { instance } = await loadWasm("/app.wasm", { env: {} });
4. Split compile from instantiate when caching
If you instantiate the same module more than once, compile a single time and reuse the Module.
const module = await WebAssembly.compileStreaming(fetch("/app.wasm"));
const inst1 = new WebAssembly.Instance(module, importObject); // fresh linear memory
const inst2 = new WebAssembly.Instance(module, importObject); // no recompile
Expected output
With DevTools open, instrument both paths and compare. Streaming overlaps network and compile, so the wall clock from request start to first export call is shorter for any non-trivial payload.
performance.mark("start");
const { instance } = await loadWasm("/app.wasm", { env: {} });
performance.mark("ready");
performance.measure("wasm-boot", "start", "ready");
console.log(performance.getEntriesByName("wasm-boot")[0].duration.toFixed(1), "ms");
// streaming, ~2 MB module: ~18.4 ms
// arraybuffer, same module: ~31.7 ms
In the Performance panel, the streaming path shows a “Compile Wasm” task that starts before the network request finishes; the ArrayBuffer path shows compilation beginning only after the request completes.
Gotchas
Streaming throws a TypeError on the wrong MIME type. If the server sends anything other than
application/wasm, you get:
TypeError: Failed to execute 'compileStreaming' on 'WebAssembly': Incorrect response MIME type.
Expected 'application/wasm'.
The fix is to serve .wasm with Content-Type: application/wasm, or rely on the ArrayBuffer fallback in
step 3, which ignores the MIME type entirely. Configuring the server correctly is covered under
local development server configurations.
A Content-Security-Policy can block compilation. Under a strict CSP, WebAssembly.instantiate/
instantiateStreaming may require script-src 'wasm-unsafe-eval' (or 'unsafe-eval' on older policies).
Without it, compilation throws a CompileError/EvalError-style violation regardless of which load path
you choose — neither streaming nor ArrayBuffer escapes a CSP that forbids Wasm compilation.
Don’t fetch twice. A naive fallback that calls fetch(url) for streaming and fetch(url) again in the
catch block doubles network traffic on the failure path. The step-3 helper only re-fetches when streaming
was unusable, which is the correct tradeoff.
Caching is about the Module, not the load path. Neither API caches by itself. To skip recompilation
on a return visit, store the compiled WebAssembly.Module in IndexedDB — it survives a structured clone —
and instantiate it directly next time, bypassing the fetch entirely.
Performance note
For a typical 1–3 MB module on a mid-tier connection, streaming removes the full compile duration from the critical path because it runs concurrently with the download, often shaving 30–50% off time-to-first-call versus the ArrayBuffer path. The savings scale with module size: the bigger the binary, the more compile work overlaps the transfer, and the larger the absolute win at the time-to-first-byte boundary.
The flip side is that streaming gives you nothing when there is nothing to overlap. A 10 KB module finishes
downloading almost instantly, so there is no meaningful window for compilation to hide inside, and the
ArrayBuffer path’s extra serial compile step is negligible. The optimization is therefore most valuable
exactly where cold start hurts most: large, compute-heavy modules on slower connections. Pair streaming
with HTTP compression (Brotli) so fewer bytes traverse the wire, and with a cached WebAssembly.Module so
that repeat visits skip compilation entirely — the two optimizations compose, and together they collapse a
multi-megabyte cold start into a near-instant warm start.
Frequently Asked Questions
Does the ArrayBuffer path need application/wasm too?
No. WebAssembly.instantiate(bytes) operates on raw bytes you already hold, so it ignores the MIME type
entirely. That is exactly why it is the right fallback when you cannot control the server’s response
headers.
Is instantiateStreaming always faster?
For anything beyond a few kilobytes, yes — it overlaps download and compilation. For a tiny inlined module
whose bytes you already have in memory, there is nothing to stream, so instantiate(bytes) is the natural
choice and the difference is negligible.
Can I use streaming in a Web Worker?
Yes. fetch and WebAssembly.instantiateStreaming are both available in worker scopes, and compiling off
the main thread keeps the UI responsive. You can also compile once and postMessage the resulting
WebAssembly.Module to other workers to avoid recompiling per worker.
My bundler inlines the .wasm as a base64 string — which path do I use?
The ArrayBuffer path. Once the bytes are already in memory as a decoded ArrayBuffer or Uint8Array,
there is no Response to stream, so call WebAssembly.instantiate(bytes, importObject) directly. Streaming
only helps when bytes are still arriving over the network; inlined modules have already finished
“downloading” by the time your script runs.
Related
- Wasm instantiation lifecycle — the full fetch → compile → instantiate pipeline this guide drills into.
- Local development server configurations — serving
.wasmwith the MIME type streaming requires. - JS/Wasm interop & memory management — what to do with
instance.exportsonce the module is live.
← Back to Wasm Instantiation Lifecycle