Loading and Running WebAssembly from JavaScript

Updated June 2026
Loading a WebAssembly module from JavaScript is a short, well-defined process: fetch the binary, compile it with WebAssembly.instantiateStreaming, provide an imports object, and then call the module's exported functions like ordinary JavaScript functions. The part that takes thought is sharing data, since you move it through the module's linear memory using typed-array views rather than passing objects. This guide walks through the whole round trip so the boundary between JavaScript and wasm is concrete.

When you use Emscripten or wasm-bindgen, most of this glue is generated for you, so you rarely write the raw loading code in production. But understanding what that glue does is what lets you debug it, tune the boundary for performance, and hand-write a lean loader when you have a small module and do not want to ship a toolchain's full runtime. This article is that understanding, written for the case where you load a module yourself.

Step 1: Fetch and Compile with instantiateStreaming

The modern way to load a module is WebAssembly.instantiateStreaming, which takes the response from a fetch of your .wasm file and compiles the binary as it downloads, overlapping compilation with transfer so the module is ready sooner. You pass it the fetch response and an imports object, and it returns a promise that resolves to an object containing the compiled module and a live instance. This single call replaces the older two-step approach of downloading the whole file and then compiling it, and it is the recommended path for nearly all cases.

The one requirement is that your server send the wasm file with the correct application/wasm content type, because streaming compilation checks it. If the type is wrong, the browser refuses to stream and you fall back to the slower approach. This is a common first-time stumbling block: the module works when loaded the slow way but fails with streaming, traced to a misconfigured content type on the host. Setting it correctly is a one-line server configuration and worth doing.

Step 2: Provide the Imports Object

A module often needs to call back into JavaScript, for logging, for browser functions, or for anything outside its sandbox, and it declares these needs as imports. You satisfy them by passing an imports object to the instantiation call, structured as a set of named functions and values the module expects. If the module imports a function to print a message, you provide that function here, and the module calls it as if it were its own. The imports object is also where a shared memory object is passed when you want JavaScript to control the memory rather than the module.

If the module declares an import you do not provide, instantiation fails with a clear error naming the missing import, which makes this easy to debug. The imports object is the inbound half of the boundary, the functions the module is allowed to call, while the exports you read next are the outbound half, the functions you are allowed to call. Together they are the complete, explicit interface between the two worlds, which is exactly the sandbox boundary described in the WebAssembly basics.

Step 3: Call Exported Functions

Once instantiated, the module's exported functions live on the instance's exports object, and you call them from JavaScript exactly like normal functions. A module that exports an add function is called as instance.exports.add(2, 3), returning 5. These calls run the compiled wasm code directly and return their results to JavaScript. For a game, the key exports are usually an initialization function, a per-frame update function, and accessors that tell JavaScript where data lives in memory.

The catch, and the reason the next steps exist, is that raw wasm functions can only take and return numbers: integers and floats. There is no passing a JavaScript array, string, or object directly. To work with anything larger than a number, you move the data through the module's memory, which is the part that makes wasm both fast and a little more involved than calling ordinary JavaScript.

Key Takeaway

Exported wasm functions only pass numbers. Anything larger, arrays, vertex buffers, world state, moves through the module's linear memory, which JavaScript reads and writes as a shared ArrayBuffer.

Step 4: Create Views Over Linear Memory

The module's memory is exposed as instance.exports.memory, a WebAssembly.Memory object whose buffer property is a plain ArrayBuffer, a flat block of bytes. To read or write meaningful values, you wrap that buffer in a typed-array view: a Float32Array to see it as 32-bit floats, an Int32Array for integers, a Uint8Array for raw bytes. The view does not copy the memory, it is a window onto the same bytes the module uses, so writing through the view changes what the module sees and reading through it sees what the module wrote.

One detail to handle is that if the module's memory grows during execution, the old ArrayBuffer is detached and you need to recreate your views over the new buffer. Many loaders create the views once after a known growth point, or recreate them when needed, to avoid reading from a stale, detached buffer. This is a frequent source of subtle bugs, a view that worked suddenly returning zeros after the module allocated more memory, so it is worth knowing the buffer can change underneath you.

Step 5: Pass Data In and Out

With a view in hand, passing data is a matter of agreeing on a location. To send an array of numbers into the module, you ask the module for a memory offset, often by calling an exported allocation function, write your numbers into the typed-array view at that offset, and call the module function passing the offset and the count. The module reads its inputs straight from its own memory. To get results back, the module writes them into memory at an offset it returns or that you agreed on, and you read them out of the view after the call. No serialization, no copying across the boundary, just shared bytes.

This is the technique that makes wasm fast for games, and it is the concrete form of the advice in the performance article to share memory rather than copy it. A physics module writes every body's position into a known buffer, and the renderer reads the whole buffer in one pass. The cost of handing thousands of positions across the boundary is the cost of agreeing on an offset, which is essentially nothing, instead of the cost of copying or serializing them every frame.

Step 6: Drive It From the Game Loop

Putting it together, a hand-written wasm game loads the module once at startup, then in its requestAnimationFrame loop calls the module's update function a single time per frame, passing the frame's input, and reads the module's output buffer to render with WebGL on the JavaScript side. The boundary is crossed roughly once per frame, the heavy simulation runs inside the module at full speed, and the rendering stays in JavaScript where it belongs. This coarse, once-per-frame pattern is what separates a fast wasm game from one that crawls under thousands of tiny boundary calls.

That is the entire round trip: fetch and compile, provide imports, call exports, view the memory, share data through it, and drive it from the loop. Whether you write this by hand for a lean module or let Emscripten and wasm-bindgen generate it, the shape is the same, and knowing it is what lets you reason about and debug any wasm game, including the ones whose loaders a toolchain produced for you.