Today, loading WebAssembly in an ES Module project uses the WebAssembly API. The recommended approach is instantiateStreaming(), which compiles and instantiates in a single step while streaming the binary:
// math.js - ESM wrapper for a WASM module
const wasmModule = await WebAssembly.instantiateStreaming(
fetch('./math.wasm')
);
// Export WASM functions as ESM exports
export const add = wasmModule.instance.exports.add;
export const multiply = wasmModule.instance.exports.multiply;
For more control, you can separate the compile and instantiate steps. This is useful when you need to pass imports to the WASM module or instantiate the same module multiple times:
// Advanced: fetch, compile, then instantiate with imports
const response = await fetch('./graphics.wasm');
const bytes = await response.arrayBuffer();
const compiled = await WebAssembly.compile(bytes);
// Pass JavaScript functions into WASM
const instance = await WebAssembly.instantiate(compiled, {
env: {
log: (value) => console.log('WASM says:', value),
memory: new WebAssembly.Memory({ initial: 256 })
}
});
export const render = instance.exports.render;
export const resize = instance.exports.resize;
Why instantiateStreaming?
WebAssembly.instantiateStreaming() is the most efficient way to load WASM. It compiles the binary while it is still downloading, avoiding the overhead of buffering the entire file first. Always prefer it over compile() + instantiate() when you do not need to pass custom imports.
Using WASM functions from an ESM consumer is seamless once wrapped:
// app.js - consuming the WASM-backed ESM module
import { add, multiply } from './math.js';
console.log(add(40, 2)); // 42
console.log(multiply(6, 7)); // 42
// The consumer does not know or care that
// the implementation uses WebAssembly
Source Phase Imports (TC39 Stage 3) introduce a new import source syntax that gives you a compiled WebAssembly.Module directly, without needing fetch or compile calls:
// Source phase import - returns a WebAssembly.Module
import source mathModule from './math.wasm';
// The module is already compiled, just instantiate it
const instance = await WebAssembly.instantiate(mathModule, {
// Optional imports object
});
export const add = instance.exports.add;
export const multiply = instance.exports.multiply;
This dramatically simplifies WASM loading compared to the fetch + compile pattern. The runtime handles fetching, streaming compilation, and caching automatically:
// Before: manual fetch and compile
const response = await fetch('./math.wasm');
const bytes = await response.arrayBuffer();
const module = await WebAssembly.compile(bytes);
const instance = await WebAssembly.instantiate(module);
// After: source phase import handles it all
import source mathModule from './math.wasm';
const instance = await WebAssembly.instantiate(mathModule);
Runtime Support:
Source phase imports also enable sharing a compiled module across multiple instantiations, which is useful for worker-based parallelism:
// Share one compiled module across multiple workers
import source imageProcessor from './image-processor.wasm';
// Each worker gets its own instance with separate memory
const worker1 = await WebAssembly.instantiate(imageProcessor, {
env: { memory: new WebAssembly.Memory({ initial: 256 }) }
});
const worker2 = await WebAssembly.instantiate(imageProcessor, {
env: { memory: new WebAssembly.Memory({ initial: 256 }) }
});
wasm-pack compiles Rust code to WebAssembly and generates ESM-compatible JavaScript bindings automatically. It is the most mature WASM toolchain:
// src/lib.rs - Rust source
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn fibonacci(n: u32) -> u32 {
match n {
0 => 0,
1 => 1,
_ => fibonacci(n - 1) + fibonacci(n - 2),
}
}
# Build with ESM output target
wasm-pack build --target web
# Generated output:
# pkg/
# my_crate.js <-- ESM wrapper (auto-generated)
# my_crate_bg.wasm <-- Compiled WASM binary
# my_crate.d.ts <-- TypeScript declarations
// Import the generated ESM wrapper
import init, { fibonacci } from './pkg/my_crate.js';
// Initialize the WASM module (required once)
await init();
console.log(fibonacci(10)); // 55
AssemblyScript uses a TypeScript-like syntax that compiles directly to WebAssembly. It is the easiest entry point for JavaScript developers:
// assembly/index.ts - AssemblyScript source
export function add(a: i32, b: i32): i32 {
return a + b;
}
export function isPrime(n: i32): bool {
if (n < 2) return false;
for (let i: i32 = 2; i * i <= n; i++) {
if (n % i === 0) return false;
}
return true;
}
# Compile with ESM bindings
npx asc assembly/index.ts --outFile build/math.wasm \
--bindings esm --optimize
// Import the generated ESM bindings
import { add, isPrime } from './build/math.js';
console.log(add(2, 3)); // 5
console.log(isPrime(17)); // true
Emscripten compiles C and C++ code to WebAssembly with full standard library support. Use -sEXPORT_ES6=1 to generate ESM output:
// math.c - C source
#include <emscripten.h>
EMSCRIPTEN_KEEPALIVE
int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
# Compile with ESM output
emcc math.c -o math.js \
-sEXPORT_ES6=1 \
-sMODULARIZE=1 \
-sEXPORTED_FUNCTIONS='["_factorial"]' \
-sEXPORTED_RUNTIME_METHODS='["ccall","cwrap"]'
// Import the ESM module generated by Emscripten
import createModule from './math.js';
const Module = await createModule();
const factorial = Module.cwrap('factorial', 'number', ['number']);
console.log(factorial(10)); // 3628800
Choosing a Toolchain:
Use wasm-pack for Rust projects with the best ESM integration and type safety. Use AssemblyScript for the lowest learning curve if you know TypeScript. Use Emscripten when porting existing C/C++ libraries or when you need full POSIX support.
WebAssembly excels where JavaScript hits performance limits. Combined with ES Modules, you get a clean integration boundary between high-performance WASM code and your JavaScript application:
Performance-Critical Computation
Physics simulations, pathfinding algorithms, data compression, and heavy number crunching. WASM runs at near-native speed - typically 1.5-2x slower than native C, but 10-100x faster than equivalent JavaScript.
Cryptography
Hashing, encryption, and digital signatures. Libraries like libsodium compile to WASM for constant-time operations that are harder to achieve in JavaScript due to JIT optimization variability.
Image & Video Processing
Real-time filters, format conversion, and compression. FFmpeg compiled to WASM enables browser-native video transcoding. Sharp and Squoosh use WASM for image optimization.
Gaming
Game engines like Unity and Unreal export to WASM. Physics engines, collision detection, and rendering pipelines benefit from predictable WASM performance without GC pauses.
AI/ML Inference in Browser
Run machine learning models client-side using WASM backends. ONNX Runtime Web and TensorFlow.js use WASM for CPU inference when WebGPU is not available.
Databases & Parsers
SQLite compiled to WASM (sql.js) runs full SQL queries in the browser. PDF parsers, markdown renderers, and syntax highlighters all benefit from WASM performance.
A common pattern is to use WASM for the hot path and JavaScript for everything else:
// image-editor.js - WASM for heavy lifting, JS for UI
import { applyFilter, resize } from './image-wasm.js';
export async function processImage(imageData, options) {
// JavaScript handles UI logic and validation
if (!imageData || imageData.length === 0) {
throw new Error('No image data provided');
}
// WASM handles the performance-critical work
const filtered = applyFilter(imageData, options.filter);
const resized = resize(filtered, options.width, options.height);
// JavaScript handles the result
return new Blob([resized], { type: 'image/png' });
}
| Feature | Chrome | Firefox | Safari | Node.js | Deno |
|---|---|---|---|---|---|
| WebAssembly Core | 57+ | 52+ | 11+ | 8+ | 1.0+ |
| instantiateStreaming() | 61+ | 58+ | 15+ | N/A (no fetch) | 1.0+ |
| Source Phase Imports | V8 M131+ | Not yet | Not yet | In progress | 2.6+ |
| WASM Threads | 74+ | 79+ | 14.1+ | 16+ | 1.0+ |
| WASM SIMD | 91+ | 89+ | 16.4+ | 16+ | 1.0+ |
| WASM GC | 119+ | 120+ | Not yet | Experimental | 1.38+ |
WASM in Node.js:
Node.js does not have a native fetch() for local files, so instantiateStreaming() does not apply. Instead, read the .wasm file with fs.readFile() and use WebAssembly.instantiate(buffer). Source phase imports will make this seamless once shipped.
Not yet with standard import syntax, but Source Phase Imports (TC39 Stage 3) enable import source mod from './mod.wasm' which gives you a compiled WebAssembly.Module. Chrome V8 M131+ and Deno 2.6+ support this. Full evaluation-phase imports are still in development.
Use WebAssembly.instantiateStreaming(fetch('./module.wasm')) for the best performance. This streams and compiles simultaneously. Wrap it in an ESM module that exports the WASM functions for clean imports throughout your project.
Use wasm-pack for Rust projects (best ESM integration and type safety). Use AssemblyScript for TypeScript-like syntax with the lowest learning curve. Use Emscripten for C/C++ code or when porting existing native libraries. All three generate ESM-compatible output.
Yes. Node.js can load WASM files using fs.readFile() and the WebAssembly API within ESM modules. Source phase imports are being implemented for Node.js as well, which will make WASM loading as simple as a single import statement.