Node.js 中运行 WebAssembly
Node.js 原生 Wasm 支持
Node.js 从 8.0 版本起就内置了 WebAssembly 支持,使用的是 V8 引擎的 Wasm 实现,与浏览器完全兼容。
// Node.js 加载 Wasm 模块
import { readFileSync } from 'node:fs';
const wasmBuffer = readFileSync('./module.wasm');
const { instance } = await WebAssembly.instantiate(wasmBuffer);
console.log(instance.exports.add(2, 3)); // 5
使用 wasm-pack 生成的 Node.js 包
# 编译为 Node.js 目标
wasm-pack build --target nodejs
// CommonJS 方式
const { add, Counter } = require('./pkg/hello_wasm.js');
console.log(add(5, 7)); // 12
在 Fastify 中使用 Wasm
// server.js:Fastify + Wasm 计算服务
import Fastify from 'fastify';
import { readFileSync } from 'node:fs';
const fastify = Fastify({ logger: true });
// 启动时加载 Wasm 模块
const wasmBuffer = readFileSync('./crypto.wasm');
const { instance: cryptoWasm } = await WebAssembly.instantiate(wasmBuffer);
fastify.post('/hash', async (request, reply) => {
const { data } = request.body;
// 使用 Wasm 计算哈希(CPU 密集型,不阻塞事件循环)
const hash = cryptoWasm.exports.sha256(data);
return { hash };
});
await fastify.listen({ port: 3000 });
Cloudflare Workers + WebAssembly
// worker.js:Cloudflare Workers 边缘 Wasm
import wasmModule from './module.wasm';
export default {
async fetch(request) {
// Cloudflare Workers 中 Wasm 实例化是同步的
const instance = new WebAssembly.Instance(wasmModule);
const result = instance.exports.compute(42);
return new Response(JSON.stringify({ result }), {
headers: { 'Content-Type': 'application/json' },
});
},
};
# wrangler.toml:Cloudflare Workers 配置
name = "my-worker"
main = "worker.js"
[[rules]]
globs = ["**/*.wasm"]
type = "CompiledWasm"
Deno 中的 WebAssembly
// Deno 与浏览器 API 完全兼容
const wasmUrl = new URL('./module.wasm', import.meta.url);
const { instance } = await WebAssembly.instantiateStreaming(fetch(wasmUrl));
console.log(instance.exports.add(1, 2));
为什么在服务端使用 WebAssembly?
服务端 Wasm 的核心价值
在服务端,WebAssembly 的主要价值不是性能(Node.js 本身已经很快),而是以下三个方面:
Node.js 中的 Wasm 架构原理
V8 引擎如何处理 Wasm
Node.js 使用 V8 引擎执行 WebAssembly,与 Chrome 浏览器共享相同的 Wasm 实现。Wasm 在 V8 中经历以下阶段:
--experimental-wasm-compilation-hints 等标志)。再次启动时直接加载缓存的机器码,跳过编译步骤,大幅加快冷启动。worker.postMessage(module, [module]) 零拷贝传给 Worker 线程,避免在每个线程重复编译相同的 Wasm 字节码。浏览器中推荐使用 WebAssembly.instantiateStreaming(fetch(url)) 流式加载,但在 Node.js 中情况不同:Node.js 18+ 的全局 fetch 不支持 file:// URL,因此本地 .wasm 文件必须用 readFileSync 或 fs.promises.readFile 读取 Buffer,再传给 instantiate()。如果你的 Wasm 来自 HTTP,则可以使用 instantiateStreaming(fetch(url))。
Node.js 加载 Wasm 的三种方式
// 方式 1:readFileSync + instantiate(同步读文件,异步编译)
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { resolve, dirname } from 'node:path';
const __dirname = dirname(fileURLToPath(import.meta.url));
const wasmPath = resolve(__dirname, './module.wasm');
const wasmBuffer = readFileSync(wasmPath); // 同步读取,返回 Buffer
const { instance } = await WebAssembly.instantiate(wasmBuffer);
// 方式 2:fetch(Node.js 18+ 支持全局 fetch)
// 注意:Node.js 中 fetch 不能使用 file:// URL
// 需要 HTTP 服务器提供 .wasm 文件
// 方式 3:动态 import(实验性,Node.js 可能支持 import('./module.wasm'))
// 目前需要开启实验标志
// node --experimental-wasm-modules app.js
在 Worker Threads 中使用 Wasm
不阻塞事件循环的 Wasm 计算
CPU 密集型的 Wasm 计算放在主线程会阻塞 Node.js 的事件循环,导致服务无响应。应使用 Worker Threads 在独立线程中运行:
// compute-worker.js — 在独立 Worker 线程中运行 Wasm
import { parentPort, workerData } from 'node:worker_threads';
import { readFileSync } from 'node:fs';
// 加载 Wasm 模块(Worker 内部,不影响主线程)
const wasmBuffer = readFileSync('./crypto.wasm');
const { instance } = await WebAssembly.instantiate(wasmBuffer);
// 接收主线程发来的任务,处理完后发回结果
parentPort.on('message', ({ data, taskId }) => {
// CPU 密集型操作在 Worker 线程,不阻塞主线程
const ptr = instance.exports.alloc(data.length);
const mem = new Uint8Array(instance.exports.memory.buffer);
mem.set(data, ptr);
const result = instance.exports.hash_sha256(ptr, data.length);
instance.exports.free(ptr);
parentPort.postMessage({ result, taskId });
});
// server.js — 主线程使用 Worker Pool
import { Worker } from 'node:worker_threads';
import Fastify from 'fastify';
// 创建 Worker 池(CPU 核数 - 1 个 Worker)
const POOL_SIZE = (os.cpus().length - 1) || 1;
const workers = Array.from({ length: POOL_SIZE },
() => new Worker('./compute-worker.js')
);
let workerIndex = 0;
function computeInWorker(data) {
return new Promise((resolve, reject) => {
const worker = workers[workerIndex++ % POOL_SIZE];
const taskId = Math.random();
worker.postMessage({ data, taskId });
worker.once('message', (msg) => {
if (msg.taskId === taskId) resolve(msg.result);
});
});
}
const app = Fastify();
app.post('/hash', async (req) => {
const data = Buffer.from(req.body.text);
const hash = await computeInWorker(data); // 不阻塞!
return { hash };
});
Wasm 模块的预编译与缓存策略
在生产环境中高效管理 Wasm 模块
在 Node.js 服务中,WebAssembly.Module(编译结果)和 WebAssembly.Instance(运行实例)是两个独立概念。合理地分离这两个步骤,可以显著提升多 Worker 场景的性能:
// production-wasm-loader.mjs
// 在服务启动时编译一次,传给所有 Worker 共用
import { readFileSync } from 'node:fs';
import { Worker } from 'node:worker_threads';
async function createWasmWorkerPool(wasmPath, poolSize) {
// 第1步:同步读取 .wasm 文件字节
const wasmBytes = readFileSync(wasmPath);
// 第2步:编译为 Module(耗时,但只做一次)
const wasmModule = await WebAssembly.compile(wasmBytes);
console.log('Wasm 编译完成,开始分发到 Worker 池');
// 第3步:将已编译的 Module 传给多个 Worker(零拷贝传输)
const workers = Array.from({ length: poolSize }, () => {
const worker = new Worker('./wasm-worker.mjs');
// Transferable:传输后主线程失去对 Module 的所有权(但可以再次 compile)
worker.postMessage({ module: wasmModule }, [wasmModule]);
return worker;
});
return workers;
}
// wasm-worker.mjs — Worker 收到已编译的 Module,直接实例化
import { parentPort } from 'node:worker_threads';
let wasmInstance;
// 接收主线程传来的预编译 Module
parentPort.once('message', async ({ module }) => {
// 直接实例化,跳过编译步骤,速度很快
const { instance } = await WebAssembly.instantiate(module, {
env: { memory: new WebAssembly.Memory({ initial: 1 }) }
});
wasmInstance = instance;
parentPort.postMessage({ ready: true });
});
// 处理实际工作任务
parentPort.on('message', ({ input, taskId }) => {
if (!wasmInstance || !input) return;
const result = wasmInstance.exports.process(input);
parentPort.postMessage({ result, taskId });
});
Deno 与 Bun 中的 WebAssembly
instantiateStreaming(fetch(url)) 加载远程或本地 Wasm(通过 import.meta.url)。Deno 还内置了 Deno.core.decode() 等底层 API,性能更好。Deno Deploy 支持在边缘节点运行 Wasm。import .wasm 文件(通过内置加载器):import module from './foo.wasm',比 Node.js 的实验性标志更稳定。// Deno:使用 import.meta.url 加载相对路径的 Wasm
const wasmUrl = new URL('./module.wasm', import.meta.url);
const { instance } = await WebAssembly.instantiateStreaming(fetch(wasmUrl));
console.log(instance.exports.add(1, 2)); // 3
// Bun:直接 import .wasm(需要 Bun 支持)
import wasmModule from './module.wasm'; // 已编译的 WebAssembly.Module
const { instance } = await WebAssembly.instantiate(wasmModule);
console.log(instance.exports.add(3, 4)); // 7
Cloudflare Workers 的 Wasm 限制
new WebAssembly.Instance(module)),因为 Worker 环境不允许顶层 await 在 fetch handler 中。服务端 Wasm 的性能基准与适用场景
何时选择 Wasm,何时直接用原生 Node.js 模块
Wasm 模块只隔离了它自己的线性内存。通过导入函数(import)传给 Wasm 的 JavaScript 对象不在隔离范围内——如果你将 Node.js 的 fs 模块方法作为导入函数传给 Wasm,恶意 Wasm 仍然可以调用这些函数访问文件系统。真正的沙箱隔离应配合 WASI 能力模型,只传递经过审查的接口。
本章小结
- 服务端 Wasm 的主要价值是安全隔离和跨语言复用,而非纯粹性能——Node.js 本身对大多数 I/O 密集型任务已够快
- Node.js 用 readFileSync + instantiate 加载 Wasm;不同于浏览器,Node.js 可以直接同步读取文件系统
- CPU 密集型 Wasm 计算应放在 Worker Threads 中,避免阻塞 Node.js 事件循环;建议使用 Worker Pool 模式
- Cloudflare Workers:通过 wrangler.toml 绑定 CompiledWasm;实例化是同步的;注意大小和 CPU 时间限制
- Deno 与浏览器 API 完全兼容,可以使用 instantiateStreaming + fetch,无需特殊处理