WebAssembly in Node.js
WebAssembly (Wasm) is a binary instruction format that V8 can execute at near-native speed, inside the same process as JavaScript but in a separate, sandboxed memory space. In Node.js you can load Wasm modules compiled from C, C++, Rust, Go, or AssemblyScript and call their exports as regular JavaScript functions. Compared to native addons, Wasm is portable (one binary runs on Linux, macOS, Windows, and in browsers), sandboxed (no direct OS access by default), and doesn't require a C++ toolchain on the end user's machine. It's the right choice for CPU-intensive computation that needs to be shared between Node and the browser, or for wrapping C/C++ code without the operational complexity of N-API.
Wasm vs native addons
Dimension | WebAssembly | Native Addon (N-API) |
|---|---|---|
Portability | Single | Must compile per platform/arch/Node version |
Sandboxing | Isolated linear memory; no direct OS access | Full OS access (same as Node process) |
OS APIs | Requires WASI or JS imports to access filesystem/network | Direct system calls, file I/O, sockets |
Performance | Near-native; some overhead for JS↔Wasm boundary calls | Native; minimal boundary overhead |
Toolchain | Emscripten (C/C++) or wasm-pack (Rust) at build time only | C++ compiler required on every build machine |
Debugging | Source maps + Chrome DevTools Wasm debugging | gdb/lldb with native symbol files |
Best for | Compute-heavy algorithms, browser+Node sharing, safe sandboxing | OS API access, hardware drivers, existing native libs |
Loading a .wasm file directly
import fs from 'node:fs'
import path from 'node:path'
// Synchronous load (fine at startup, don't use in request handlers):
const wasmBuffer = fs.readFileSync(path.join(__dirname, 'math.wasm'))
const wasmModule = new WebAssembly.Module(wasmBuffer)
const instance = new WebAssembly.Instance(wasmModule, {
// imports: functions your Wasm module calls into JavaScript
env: {
log: (value: number) => console.log('Wasm log:', value),
},
})
// Call exports (functions compiled into the .wasm):
const exports = instance.exports as { add: (a: number, b: number) => number }
console.log(exports.add(3, 4)) // 7// Asynchronous loading — preferred for large modules; non-blocking:
async function loadWasm(wasmPath: string) {
const buffer = await fs.promises.readFile(wasmPath)
const { instance } = await WebAssembly.instantiate(buffer, {
env: { memory: new WebAssembly.Memory({ initial: 1 }) },
})
return instance
}Compiling C to Wasm with Emscripten
# Install Emscripten SDK (one-time): git clone https://github.com/emscripten-core/emsdk.git cd emsdk && ./emsdk install latest && ./emsdk activate latest && source ./emsdk_env.sh # Compile a C file to Wasm: emcc math.c -o math.js \ -s EXPORTED_FUNCTIONS='["_add","_multiply"]' \ -s EXPORTED_RUNTIME_METHODS='["ccall","cwrap"]' \ -s MODULARIZE=1 \ -s EXPORT_NAME=createMathModule
// math.c
#include <stdint.h>
int32_t add(int32_t a, int32_t b) { return a + b; }
int32_t multiply(int32_t a, int32_t b) { return a * b; }// Use the generated JS loader (Emscripten wraps the Wasm with a JS module):
const createMathModule = require('./math.js')
const Module = await createMathModule()
const add = Module.cwrap('add', 'number', ['number', 'number'])
console.log(add(6, 7)) // 42Compiling Rust to Wasm with wasm-pack
# Install Rust and wasm-pack: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh cargo install wasm-pack # Compile: wasm-pack build --target nodejs # generates pkg/ with .wasm + JS bindings + TypeScript types
// src/lib.rs
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn fibonacci(n: u32) -> u64 {
match n {
0 => 0,
1 => 1,
_ => fibonacci(n - 1) + fibonacci(n - 2),
}
}// After wasm-pack build --target nodejs:
const { fibonacci } = require('./pkg/my_crate')
console.log(fibonacci(40)) // fast, Rust-speedSharing memory between JS and Wasm
// Wasm has its own linear memory (a resizable ArrayBuffer).
// To pass large data without copying, write directly into Wasm memory:
const memory = new WebAssembly.Memory({ initial: 10, maximum: 100 }) // pages × 64KB
const { instance } = await WebAssembly.instantiate(wasmBuffer, {
env: { memory },
})
const exports = instance.exports as {
processData: (ptr: number, length: number) => number
}
// Write data into Wasm's linear memory via a Uint8Array view:
const view = new Uint8Array(memory.buffer)
const inputData = new TextEncoder().encode('hello wasm')
view.set(inputData, 0) // write at offset 0
// Call Wasm function with pointer (offset) and length:
const result = exports.processData(0, inputData.length)WASI — WebAssembly System Interface
import { WASI } from 'node:wasi'
import fs from 'node:fs'
// WASI gives a Wasm module controlled access to filesystem and other OS APIs:
const wasi = new WASI({
version: 'preview1',
args: process.argv,
env: process.env,
preopens: {
'/sandbox': '/some/real/directory', // map virtual path to real path
},
})
const wasmBuffer = fs.readFileSync('cli-tool.wasm')
const { instance } = await WebAssembly.instantiate(wasmBuffer, {
wasi_snapshot_preview1: wasi.wasiImport,
})
wasi.start(instance) // run the Wasm module's _start / main function