Instantiating One Module Many Times

This page answers one task: run several independent copies of the same WebAssembly module — one per worker, per document, per plugin run or per request — without paying for compilation more than once.

Prerequisites

Module versus instance

The JavaScript API separates two objects that are easy to conflate. A WebAssembly.Module is compiled code: the validated, translated machine code for every function, plus the module’s declarations. It is stateless and immutable, which is why it can be cached, posted between workers and reused. A WebAssembly.Instance is one running copy: its own linear memory, its own tables, its own mutable globals, its own binding of imports. Two instances of the same module share code and share nothing else.

That makes “compile once, instantiate many times” the natural pattern for anything that needs isolation between runs. Each instance starts from the module’s initial state — data segments written into a fresh memory, globals at their initial values — so a run cannot see leftovers from another run. For untrusted input, multi-tenant hosts and run-to-completion programs, that clean slate is the point.

What instances share and what each has of its own All instances of a module share the compiled code and declarations. Each instance has its own linear memory, tables, mutable globals and import bindings, so state in one instance is invisible to the others. WebAssembly.Module (shared) validated, compiled machine code; immutable; postable instance A own memory, tables, globals, imports instance B own memory, tables, globals, imports instance C own memory, tables, globals, imports

Step 1 — compile once, keep the Module

const modulePromise = WebAssembly.compileStreaming(fetch("/wasm/converter.wasm"));

async function runJob(input) {
  const module = await modulePromise;
  const { exports } = await WebAssembly.instantiate(module, makeImports());   // fresh state per job
  return convert(exports, input);
}

WebAssembly.instantiate with a Module (rather than bytes) resolves to an Instance directly and skips compilation entirely. Keep the module promise at module scope so every job awaits the same compilation.

Step 2 — share the compiled module with workers

A WebAssembly.Module can be sent to workers with postMessage. The worker receives a handle to the same compiled code, not a copy of the bytes to recompile:

// main thread
const module = await WebAssembly.compileStreaming(fetch("/wasm/engine.wasm"));
const workers = Array.from({ length: navigator.hardwareConcurrency }, () => {
  const w = new Worker(new URL("./engine.worker.js", import.meta.url), { type: "module" });
  w.postMessage({ type: "init", module });
  return w;
});
// engine.worker.js
let exports;
self.onmessage = async ({ data }) => {
  if (data.type === "init") {
    ({ exports } = await WebAssembly.instantiate(data.module, makeImports()));
  } else if (data.type === "job") {
    self.postMessage(exports.process(data.payload));
  }
};

Eight workers compiling the module themselves would do eight compilations; posting the module does one. Each worker still has its own instance and memory, so they run truly independently. Sharing memory between them as well is a different design — threads — covered in sharing memory between Wasm and Web Workers.

One compile, many instances across workers The main thread compiles the module once and posts the Module object to each worker. Each worker instantiates it with its own imports and memory, without recompiling, and processes jobs independently. main thread worker 1 worker 2 compileStreaming → Module postMessage({ module }) postMessage({ module }) instantiate(module) — no compile instantiate(module) — no compile

Step 3 — choose between reusing and recreating instances

There are two strategies for repeated jobs. Reuse one instance for many jobs: fastest, because the memory is already allocated and warmed, but state accumulates — allocator fragmentation, caches, any global the code forgot to reset — and a trap in one job can leave the instance inconsistent. Recreate an instance per job: every job starts clean, a failure cannot leak into the next job, and memory is released when the instance is garbage-collected; the cost is instantiation and the memory’s first-touch page faults.

// reuse: one long-lived instance
const shared = await WebAssembly.instantiate(await modulePromise, makeImports());
function jobReuse(input) { return convert(shared.exports, input); }

// recreate: fresh instance per job
async function jobFresh(input) {
  const inst = await WebAssembly.instantiate(await modulePromise, makeImports());
  return convert(inst.exports, input);
}

A practical middle ground is to reuse an instance until a job fails or a counter or memory threshold is reached, then replace it — the approach described in recovering a module after a trap.

Reusing one instance versus a fresh instance per job Reusing an instance is fastest but carries state and failures between jobs. A fresh instance per job costs instantiation time and initial memory but isolates every job completely. reuse one instance no instantiation per job warm memory and allocator state and fragmentation accumulate a trap may poison later jobs trusted, uniform workloads fresh instance per job tens of microseconds per job clean memory every time failures cannot leak memory freed with the instance untrusted input, multi-tenant

Step 4 — mind the memory per instance

Each instance allocates its own memory at its initial size, so ten instances of a module with a 16 MB initial memory reserve 160 MB. Size initial memory for the common case and let it grow, as discussed in sizing initial and maximum memory. When instances are recreated per job, drop references to finished instances promptly so the garbage collector can release their memories — an array of old instances kept “for debugging” is a common leak.

Step 5 — share the module across page loads too

Within a page, keeping the Module object avoids recompiling. Across page loads, the browser’s compiled-code cache does the same job for modules fetched from a stable URL, so a returning user’s first compileStreaming is fast. Combine both — a cacheable URL and a single compile per page — and the compile cost is paid at most once per release per user. The caching side is covered in caching compiled Wasm modules in IndexedDB.

Designing modules that are cheap to instantiate

How expensive an instance is depends on the module as much as on the engine, and a few design choices keep instantiation in the tens-of-microseconds range. The largest is initial memory: every instance allocates its declared initial size, and the engine must zero it, so a module that asks for 64 MB up front pays for 64 MB per instance even if a job uses 2 MB. Keep initial memory small for modules that are instantiated often, and let them grow on demand.

Data segments are the next cost. Active data segments are copied into each new memory at instantiation; a module that embeds a 5 MB lookup table copies 5 MB per instance. Where the data is read-only, consider keeping it outside the module — passed in from the host once and shared — or loading it lazily with passive segments, as described in using bulk memory operations.

Then there is start-up code. A start function or an _initialize export that builds tables, parses configuration or warms caches runs for every instance. If that work produces the same result every time, it is a candidate for doing at build time — generating the table into a data segment — or for snapshotting tools that capture an initialised module, so that each instance starts already initialised. The more of a module’s state is decided at build time, the more cheaply it can be multiplied at run time.

Instances in server-side runtimes

The same separation exists outside browsers, often with more control. wasmtime’s Module is compiled once and shared across threads, and each Store holds instances; a server can create a fresh store and instance per request in microseconds, which is how serverless platforms give each request a clean sandbox. Pre-instantiation features go further, preparing linkers and resolving imports in advance so only memory setup remains per request. The pattern — compile once, isolate per unit of work — is identical; only the APIs differ, as described in embedding wasmtime in a Rust application.

Expected output

Timing the two strategies in a page:

compile once:              74 ms
instantiate (per job):     0.06 ms
job, reused instance:      1.8 ms
job, fresh instance:       1.9 ms

Gotchas

  • Instantiating from bytes every time. WebAssembly.instantiate(bytes) compiles again. Pass the Module.
  • Posting bytes to workers instead of the Module. Each worker recompiles. Post the compiled Module.
  • Assuming instances share globals. Mutable globals are per instance. Share state through imports or memory deliberately.
  • Different import objects per instance by accident. A factory that returns a shared object binds every instance to the same callbacks and state. Create a fresh import object per instance when imports hold per-run state.
  • Keeping finished instances alive. Their memories stay allocated. Drop references when done.

Performance note

For a 1.4 MB module, compiling took 74 ms on a laptop and instantiating from the compiled module took about 60 µs. Recreating the instance per job added about 3% to a 2 ms job — a small price for complete isolation. On a phone, compile was 410 ms and instantiation 0.3 ms, which makes compiling once even more important there.

Compile once versus compile per worker, eight workers Total time spent compiling a 1.4 MB module across eight workers on a laptop when each worker compiles its own copy, and when the main thread compiles once and posts the Module. total compile time across workers (ms) each worker compiles 590 ms compile once, post Module 74 ms

Frequently Asked Questions

Can a Module be stored in IndexedDB? Browsers no longer support storing WebAssembly.Module objects in IndexedDB; rely on the HTTP cache and the engine’s code cache instead.

Is instantiation thread-safe? Yes. Instantiating the same Module concurrently in several workers is the intended use.

Do instances share the code cache’s tier-up? Optimized code produced for one instance’s hot functions is part of the module’s compiled code, so other instances in the same process benefit.

Can instances of different modules share a memory? Yes, if both import it. That is how dynamic linking and some multi-module designs work; it trades isolation for shared data.

How many instances can a page run? Memory is the limit, not the engine. Each instance costs its memory plus a small fixed overhead.

← Back to Wasm Instantiation Lifecycle