Fetching WebAssembly modules without blocking the main thread

Nearly every real WebAssembly workload follows the same shape: download a .wasm file, compile it into a module, instantiate that module, and call into its exports from JavaScript. The straightforward version of that flow works, but it leaves performance on the table in two ways: synchronous APIs can stall the main thread, and waiting for the full download to finish before starting compilation lengthens the total load time.

A naive implementation reads the fetched bytes into an ArrayBuffer, then hands that buffer to new WebAssembly.Module(buffer) to get a compiled module. That instance of the constructor is synchronous. In Chrome, this API is disabled for buffers larger than 4 KB to discourage blocking the main thread on large payloads, so this pattern can outright fail in real applications.

Making module compilation asynchronous

The first fix is to replace the synchronous module constructor with the promise-based WebAssembly.compile(buffer). That change makes compilation asynchronous, but instantiating still relies on new WebAssembly.Instance(module), which has the same synchronous behavior and 4 KB limitation. The consistent choice is to use the asynchronous WebAssembly.instantiate(module) for this step.

Both of those APIs solve the main-thread blocking problem, but they still can't begin compiling until the network fetch has finished and the full buffer is materialized.

Streaming compilation and instantiation

Streaming compilation lets the browser start compiling the WebAssembly module while its bytes are still in flight from the server. Because download and compile happen in parallel, this is noticeably faster for large modules. To take advantage of it, pass the Response object from fetch() directly to WebAssembly.compileStreaming() instead of reading it into a buffer first:

const response = await fetch('module.wasm');
const module = await WebAssembly.compileStreaming(response);

That API also accepts the promise returned by fetch() directly, so an intermediate response binding is unnecessary if you don't use it elsewhere. If you don't need the fetch result for anything else either, you can inline the call. Keeping the fetch on its own line tends to read more clearly.

Combining compilation and instantiation

The flow so far separates compilation from instantiation. WebAssembly.instantiate() can do both in one call, and the streaming variant — WebAssembly.instantiateStreaming() — applies the same download/compile overlap to that combined operation. When a single instance is all you need, this eliminates the temporary module variable entirely:

const instance = await WebAssembly.instantiateStreaming(fetch('module.wasm'));

The practical takeaway is three rules of thumb:

  • Prefer asynchronous WebAssembly APIs to keep the main thread responsive.
  • Use the streaming APIs when the destination is a network Response, since compilation starts before the payload fully arrives.
  • Drop the intermediate steps — module variables, buffers, and fetches — that your code doesn't actually use.