WebAssembly JavaScript API
加载 Wasm 模块
// 方法1:instantiateStreaming(推荐,流式加载)
const { instance, module } = await WebAssembly.instantiateStreaming(
fetch('./module.wasm'),
importObject // 导入对象(可选)
);
// 方法2:instantiate(需要先获取 ArrayBuffer)
const response = await fetch('./module.wasm');
const buffer = await response.arrayBuffer();
const { instance } = await WebAssembly.instantiate(buffer, importObject);
WebAssembly.Memory:线性内存
// 创建 Wasm 内存(1 页 = 64 KB)
const memory = new WebAssembly.Memory({
initial: 1, // 初始 1 页(64 KB)
maximum: 10, // 最大 10 页(640 KB)
});
// 通过 TypedArray 视图操作内存
const bytes = new Uint8Array(memory.buffer);
const ints = new Int32Array(memory.buffer);
const floats = new Float64Array(memory.buffer);
// 写入数据
ints[0] = 42;
ints[1] = 100;
// 调用 Wasm 函数处理内存中的数据
instance.exports.process(0, 2); // offset, count
// 读取 Wasm 写入的结果
console.log(ints[2]); // 读取结果
在 JS 和 Wasm 之间传递字符串
// Wasm 没有原生字符串类型,需要通过内存传递
// 将 JS 字符串写入 Wasm 内存
function writeStringToMemory(str, ptr, memory) {
const encoder = new TextEncoder();
const encoded = encoder.encode(str);
const bytes = new Uint8Array(memory.buffer);
bytes.set(encoded, ptr);
bytes[ptr + encoded.length] = 0; // null terminator
return encoded.length;
}
// 从 Wasm 内存读取字符串
function readStringFromMemory(ptr, memory) {
const bytes = new Uint8Array(memory.buffer);
let end = ptr;
while (bytes[end] !== 0) end++;
const decoder = new TextDecoder();
return decoder.decode(bytes.subarray(ptr, end));
}
SharedArrayBuffer 与多线程
// 共享内存(需要 COOP/COEP 头部)
const sharedMemory = new WebAssembly.Memory({
initial: 1,
shared: true, // SharedArrayBuffer
maximum: 10,
});
// 主线程
const worker = new Worker('worker.js');
worker.postMessage({ memory: sharedMemory });
// worker.js
self.addEventListener('message', async ({ data }) => {
const { instance } = await WebAssembly.instantiate(wasmBuffer, {
env: { memory: data.memory }
});
// Worker 中使用相同的内存
instance.exports.compute();
});
instantiateStreaming 需要正确的 Content-Type
WebAssembly.instantiateStreaming() 要求服务器返回 Content-Type: application/wasm 响应头。如果服务器返回 application/octet-stream(常见于未配置的静态服务器),浏览器会拒绝流式处理并抛出 TypeError。本地开发时可改用 instantiate(await response.arrayBuffer()) 绕过此限制,但部署时应确保服务器配置正确。
WebAssembly JavaScript API 完整参考
WebAssembly 全局对象的核心方法
WebAssembly.instantiateStreaming()
推荐的加载方式,在下载的同时进行编译(流式处理)。比 instantiate() 快,因为不需要等待完整下载。要求服务器正确返回
Content-Type: application/wasm。WebAssembly.instantiate()
接受 ArrayBuffer(已下载的 Wasm 字节)或 WebAssembly.Module 对象。适用于需要在实例化之前检查或缓存 Module 的场景。
WebAssembly.compile()
只编译,不实例化。生成可重复使用的 WebAssembly.Module 对象。适合需要创建多个实例(如多 Worker)的场景。
WebAssembly.validate()
验证 ArrayBuffer 是否是合法的 Wasm 字节码,返回 boolean。可用于在加载前快速检查文件完整性。
导入对象(importObject)详解
// importObject 的完整结构示例
const importObject = {
// 对应 WAT 中的 (import "env" "...")
env: {
// 导入函数
log: (value) => console.log(value),
// 导入内存(预创建,与 Wasm 共享)
memory: new WebAssembly.Memory({ initial: 1 }),
// 导入 Table(函数引用表)
table: new WebAssembly.Table({ initial: 10, element: 'anyfunc' }),
// 导入全局变量
__stack_pointer: new WebAssembly.Global({ value: 'i32', mutable: true }, 65536),
},
// 对应 WAT 中的 (import "console" "...")
console: {
warn: (ptr, len) => {
// ptr/len 是 Wasm 内存中字符串的偏移和长度
const bytes = new Uint8Array(memory.buffer, ptr, len);
console.warn(new TextDecoder().decode(bytes));
}
},
};
const { instance } = await WebAssembly.instantiateStreaming(
fetch('./module.wasm'),
importObject
);
线性内存操作深度指南
内存页与增长
Wasm 内存以页(Page)为单位管理,每页固定 64 KB(65536 字节)。最多可以有 65536 页(合计 4 GB,这是 32 位地址空间上限)。
// 内存管理的完整示例
const memory = new WebAssembly.Memory({
initial: 2, // 初始 2 页 = 128 KB
maximum: 100, // 最大 100 页 = 6.4 MB
// 不设置 maximum 则可增长到 2GB(浏览器限制)
});
console.log(memory.buffer.byteLength); // 131072(128 KB)
console.log(memory.buffer.byteLength / 65536); // 2 页
// 增长内存(每次增加 1 页)
const prevPages = memory.grow(1); // 返回增长前的页数
console.log(prevPages); // 2
console.log(memory.buffer.byteLength / 65536); // 3 页
// ⚠️ 内存增长后,旧的 TypedArray 视图全部失效!
// 必须重新创建视图
let view = new Int32Array(memory.buffer);
memory.grow(1);
// view 现在已经失效,访问它是危险的!
view = new Int32Array(memory.buffer); // 必须重新创建
TypedArray 视图类型选择
Uint8Array
最通用的视图,以字节为单位读写内存。适合字符串处理(UTF-8)、原始字节操作。每元素 1 字节,索引 0 对应内存偏移 0。
Int32Array / Uint32Array
4 字节整数视图。Int32Array[1] 对应内存偏移 4(字节偏移 = 索引 × 4)。用于整数数组和 Wasm i32 数据。注意:内存地址必须是 4 的倍数(对齐要求)。
Float64Array
8 字节双精度浮点视图,与 Wasm f64 直接对应。索引 × 8 = 字节偏移。用于科学计算数据。内存地址必须是 8 的倍数。
DataView
最灵活的视图,可以以任意偏移读写任意类型,支持显式指定字节序(大端/小端)。适合处理二进制协议数据。
// 内存地址对齐的重要性
const mem = new WebAssembly.Memory({ initial: 1 });
const bytes = new Uint8Array(mem.buffer);
const ints = new Int32Array(mem.buffer);
// 正确:地址 0 是 4 字节对齐的
ints[0] = 42; // 写入字节 0-3
ints[1] = 100; // 写入字节 4-7
// 用 DataView 读取任意偏移(不对齐)
const dv = new DataView(mem.buffer);
// 从偏移 1(不对齐)读取 i32(某些平台会更慢)
dv.setInt32(1, 255, true); // true = 小端字节序
console.log(dv.getInt32(1, true)); // 255
内存增长使所有 TypedArray 视图失效
这是服务端和浏览器中都会遇到的经典 bug:在创建了 new Uint8Array(memory.buffer) 视图后,如果 Wasm 代码内部调用了 memory.grow()(因为 Vec 扩容、String 分配等),原来的 ArrayBuffer 会被分离(detached),视图会失效。访问失效的视图会返回 0,写入也会被忽略(严格模式抛出 TypeError)。解决方法:每次调用 Wasm 函数后,都通过 new Uint8Array(memory.buffer) 重新获取视图,或将视图的获取封装为函数而非常量。
多线程 Wasm:SharedArrayBuffer 详解
为什么 Web 多线程需要特殊处理
浏览器的安全机制(Spectre 攻击缓解)要求:任何使用 SharedArrayBuffer 的页面必须是跨源隔离(Cross-Origin Isolated)的。这通过设置两个 HTTP 响应头实现:
# 服务器响应头(Nginx 示例)
add_header Cross-Origin-Opener-Policy "same-origin";
add_header Cross-Origin-Embedder-Policy "require-corp";
# 验证是否已启用跨源隔离
# 在浏览器控制台执行:
# console.log(window.crossOriginIsolated); // 应该为 true
Wasm + Web Workers 并行计算
// main.js — 主线程
// 创建共享内存和 Wasm 模块
const sharedMemory = new WebAssembly.Memory({
initial: 10,
maximum: 100,
shared: true, // ← 关键:创建 SharedArrayBuffer 支持的内存
});
// 编译 Wasm 模块(只编译一次,传给多个 Worker)
const wasmModule = await WebAssembly.compileStreaming(fetch('./worker.wasm'));
// 创建 4 个 Worker,每个处理数据的 1/4
const WORKERS = 4;
const workers = Array.from({ length: WORKERS }, (_, i) => {
const worker = new Worker('./worker.js');
worker.postMessage({
// 传递已编译的 Module 和共享内存(零拷贝!)
module: wasmModule,
memory: sharedMemory,
workerId: i,
totalWorkers: WORKERS,
});
return worker;
});
// 等待所有 Worker 完成
await Promise.all(workers.map(w =>
new Promise(resolve => w.addEventListener('message', resolve, { once: true }))
));
// worker.js — Worker 线程
self.addEventListener('message', async ({ data }) => {
const { module, memory, workerId, totalWorkers } = data;
// 从已编译的 Module 创建实例(各 Worker 独立实例,但共享内存)
const instance = await WebAssembly.instantiate(module, {
env: { memory }
});
// 每个 Worker 处理数组的不同部分
instance.exports.processChunk(workerId, totalWorkers);
self.postMessage({ done: true, workerId });
});
本章小结
本章核心要点
- instantiateStreaming 是首选:边下载边编译,需要正确的 Content-Type: application/wasm 响应头
- 内存以 64KB 页为单位:通过 memory.grow(n) 增长;内存增长后所有现有 TypedArray 视图都会失效,必须重新创建
- 字符串传递:Wasm 没有原生字符串,需通过 TextEncoder/TextDecoder 和内存偏移传递;这是 JS↔Wasm 最主要的开销来源
- 多线程需要跨源隔离:SharedArrayBuffer 要求设置 COOP 和 COEP 响应头;可传递已编译的 Module 给多个 Worker 避免重复编译
- 对齐很重要:Int32Array 索引 × 4 = 字节偏移,不对齐访问可能引起性能问题(但不会崩溃)