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
- [ ] A module whose state lives in its own memory and globals (most do).
- [ ] An understanding of the compile/instantiate split, as in streaming instantiation vs ArrayBuffer instantiation.
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.
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.
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.
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 theModule. - 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.
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.
Related
- Compiling Wasm in a worker to free the main thread — compiling off the main thread first.
- Building a Wasm thread pool — instances sharing one memory instead.
- Running WASI modules in the browser — an instance per run in practice.
- Loading untrusted plugins safely — per-run isolation for plugins.
← Back to Wasm Instantiation Lifecycle