08 - JavaScript 集成
08 - JavaScript 集成
WebAssembly 的价值只有与 JavaScript 配合才能完全发挥——Wasm 负算力,JS 负生态。
8.1 模块加载与编译
同步 vs 异步 API
// ❌ 同步编译(阻塞主线程,不推荐用于大型模块)
const module = new WebAssembly.Module(buffer);
const instance = new WebAssembly.Instance(module, imports);
// ✅ 异步编译(推荐)
const module = await WebAssembly.compile(buffer);
const instance = await WebAssembly.instantiate(module, imports);
// ✅✅ 流式编译(最高效,直接从响应流编译)
const { instance } = await WebAssembly.instantiateStreaming(
fetch('module.wasm'), imports
);
加载方式对比
| 方式 | 适用场景 | 性能 | 浏览器支持 |
|---|---|---|---|
compile() + instantiate() | 需要复用编译结果 | ✅ 高 | 全部 |
instantiate(buffer) | 简单场景 | ⚠️ 中 | 全部 |
instantiateStreaming() | 从网络加载 | ✅✅ 最高 | 现代浏览器 |
compileStreaming() | Worker 间传递 | ✅ 高 | 现代浏览器 |
预编译与缓存
// 缓存编译好的模块到 IndexedDB
async function cachedCompile(url) {
const cacheKey = `wasm-module:${url}`;
const db = await openDB('wasm-cache', 1);
// 尝试从缓存读取
const cached = await db.get('modules', cacheKey);
if (cached) {
return cached;
}
// 编译并缓存
const response = await fetch(url);
const module = await WebAssembly.compileStreaming(response);
await db.put('modules', module, cacheKey);
return module;
}
// 配合 Cache Storage API
async function cachedCompileWithCache(url) {
const cache = await caches.open('wasm-modules');
let response = await cache.match(url);
if (!response) {
response = await fetch(url);
cache.put(url, response.clone());
}
return WebAssembly.compileStreaming(response);
}
8.2 调用 Wasm 函数
基本调用
const { instance } = await WebAssembly.instantiateStreaming(
fetch('math.wasm'),
imports
);
// 直接调用导出函数
const result = instance.exports.add(10, 20);
console.log(result); // 30
// 导出的内存和表
const memory = instance.exports.memory;
const table = instance.exports.table;
处理 i64 类型
// ⚠️ i64 在 JS 中被映射为 BigInt
const { instance } = await WebAssembly.instantiateStreaming(
fetch('bigint.wasm')
);
// i64 参数和返回值需要使用 BigInt
const result = instance.exports.multiply(BigInt(100), BigInt(200));
console.log(Number(result)); // 20000
// BigInt 和 Number 互转
const bigVal = BigInt(Number.MAX_SAFE_INTEGER) * 2n;
const numVal = Number(bigVal); // 可能丢失精度
8.3 内存共享
JS 与 Wasm 共享线性内存
// Wasm 导出了 memory
const memory = instance.exports.memory;
// 创建视图读写 Wasm 内存
const i32View = new Int32Array(memory.buffer);
const f64View = new Float64Array(memory.buffer);
const u8View = new Uint8Array(memory.buffer);
// 写入数据到 Wasm 内存
const offset = 1024;
i32View[offset / 4] = 42;
f64View[offset / 8] = 3.14;
// 传递指针给 Wasm 函数
const result = instance.exports.process_data(offset);
内存增长处理
// ⚠️ 关键:memory.grow() 后 buffer 会变
let memory = instance.exports.memory;
let view = new Uint8Array(memory.buffer);
// 调用可能增长内存的函数
instance.exports.alloc(65536);
// buffer 可能已经变了!必须重新创建视图
view = new Uint8Array(memory.buffer);
// 安全的读取函数
function safeRead(memory, byteOffset, length) {
// 每次都创建新的视图
return new Uint8Array(memory.buffer, byteOffset, length);
}
传递字符串到 Wasm
// 方式 1:使用工具函数
function copyStringToWasm(str, instance) {
const encoder = new TextEncoder();
const encoded = encoder.encode(str + '\0');
const ptr = instance.exports.malloc(encoded.length);
const memView = new Uint8Array(instance.exports.memory.buffer);
memView.set(encoded, ptr);
return ptr;
}
function readStringFromWasm(ptr, instance) {
const memView = new Uint8Array(instance.exports.memory.buffer);
let end = ptr;
while (memView[end] !== 0) end++;
const decoder = new TextDecoder();
return decoder.decode(memView.slice(ptr, end));
}
// 使用
const ptr = copyStringToWasm("Hello", instance);
instance.exports.process(ptr);
const result = readStringFromWasm(ptr, instance);
instance.exports.free(ptr);
结构化数据传递
// 将 JS 对象序列化到 Wasm 内存
function writeStruct(instance, offset, struct) {
const view = new DataView(instance.exports.memory.buffer);
view.setFloat64(offset, struct.x, true); // 偏移 0: x (f64)
view.setFloat64(offset + 8, struct.y, true); // 偏移 8: y (f64)
view.setInt32(offset + 16, struct.id, true); // 偏移 16: id (i32)
view.setUint8(offset + 20, struct.active ? 1 : 0); // 偏移 20: active (bool)
}
// 使用
const ptr = instance.exports.malloc(24);
writeStruct(instance, ptr, { x: 1.0, y: 2.0, id: 42, active: true });
instance.exports.process_entity(ptr);
8.4 Web Worker 中使用 Wasm
基础 Worker + Wasm
// main.js
const worker = new Worker('worker.js');
worker.postMessage({ type: 'init' });
worker.onmessage = (e) => {
if (e.data.type === 'ready') {
worker.postMessage({
type: 'compute',
data: new Float64Array([1, 2, 3, 4, 5])
});
}
if (e.data.type === 'result') {
console.log('Result:', e.data.value);
}
};
// worker.js
let wasmInstance = null;
self.onmessage = async (e) => {
const { type, data } = e.data;
switch (type) {
case 'init': {
const imports = { /* ... */ };
const { instance } = await WebAssembly.instantiateStreaming(
fetch('compute.wasm'), imports
);
wasmInstance = instance;
self.postMessage({ type: 'ready' });
break;
}
case 'compute': {
// 将数据复制到 Wasm 内存
const memory = wasmInstance.exports.memory;
const ptr = wasmInstance.exports.malloc(data.byteLength);
new Float64Array(memory.buffer).set(data, ptr / 8);
const result = wasmInstance.exports.process(ptr, data.length);
self.postMessage({ type: 'result', value: result });
wasmInstance.exports.free(ptr);
break;
}
}
};
多 Worker 并行
// 创建 Worker 池
class WasmWorkerPool {
constructor(wasmUrl, workerCount = navigator.hardwareConcurrency) {
this.workers = [];
this.taskQueue = [];
this.nextWorker = 0;
for (let i = 0; i < workerCount; i++) {
const worker = new Worker('worker.js');
worker.postMessage({ type: 'init', wasmUrl });
worker.onmessage = (e) => this.handleResult(e);
this.workers.push(worker);
}
}
async compute(data) {
return new Promise((resolve) => {
this.taskQueue.push({ data, resolve });
const worker = this.workers[this.nextWorker % this.workers.length];
this.nextWorker++;
worker.postMessage({ type: 'compute', data });
});
}
handleResult(e) {
const task = this.taskQueue.shift();
task.resolve(e.data.value);
}
}
// 使用
const pool = new WasmWorkerPool('heavy_compute.wasm', 4);
const results = await Promise.all([
pool.compute(chunk1),
pool.compute(chunk2),
pool.compute(chunk3),
pool.compute(chunk4),
]);
8.5 SharedArrayBuffer 共享内存
// main.js — 创建共享内存
const memory = new WebAssembly.Memory({
initial: 1,
maximum: 16,
shared: true // ⚠️ 需要 COOP/COEP 头
});
// 在多个 Worker 间共享同一块内存
const worker1 = new Worker('worker.js');
const worker2 = new Worker('worker.js');
worker1.postMessage({ memory, workerId: 0 });
worker2.postMessage({ memory, workerId: 1 });
// ⚠️ 需要服务器设置以下 HTTP 头:
// Cross-Origin-Opener-Policy: same-origin
// Cross-Origin-Embedder-Policy: require-corp
// worker.js
self.onmessage = async (e) => {
const { memory, workerId } = e.data;
const imports = {
env: {
memory: memory,
worker_id: workerId
}
};
const { instance } = await WebAssembly.instantiateStreaming(
fetch('parallel.wasm'), imports
);
// 多个 Worker 并行处理同一块内存的不同区域
const offset = workerId * 1024 * 64; // 每个 Worker 64KB 区域
instance.exports.process(offset);
};
8.6 模块化集成(ESM + Bundler)
Vite 项目
// vite.config.js
export default {
optimizeDeps: {
exclude: ['my-wasm-pkg']
},
plugins: [{
name: 'wasm',
async configResolved(config) {
// 确保 .wasm 文件正确处理
}
}]
};
// 使用 wasm-pack 输出的 ESM 模块
import init, { process_image } from 'my-wasm-pkg';
let initialized = false;
export async function ensureInit() {
if (!initialized) {
await init();
initialized = true;
}
}
export async function processImage(imageData) {
await ensureInit();
return process_image(imageData);
}
Webpack 配置
// webpack.config.js
module.exports = {
experiments: {
asyncWebAssembly: true
},
module: {
rules: [
{
test: /\.wasm$/,
type: 'webassembly/async'
}
]
}
};
8.7 错误处理
async function safeInstantiate(wasmUrl, imports = {}) {
try {
// 流式编译
const response = await fetch(wasmUrl);
if (!response.ok) {
throw new Error(`Failed to fetch Wasm: ${response.status}`);
}
const contentType = response.headers.get('content-type');
if (!contentType?.includes('application/wasm')) {
console.warn('Incorrect MIME type, falling back to buffer');
const buffer = await response.arrayBuffer();
return await WebAssembly.instantiate(buffer, imports);
}
return await WebAssembly.instantiateStreaming(response, imports);
} catch (error) {
if (error instanceof WebAssembly.CompileError) {
console.error('Wasm 编译失败:', error.message);
} else if (error instanceof WebAssembly.LinkError) {
console.error('Wasm 链接失败(导入不匹配):', error.message);
} else if (error instanceof WebAssembly.RuntimeError) {
console.error('Wasm 运行时错误:', error.message);
} else {
console.error('未知错误:', error);
}
throw error;
}
}
常见错误类型
| 错误类型 | 原因 | 解决方案 |
|---|---|---|
CompileError | Wasm 二进制无效 | 检查文件是否损坏 |
LinkError | 导入函数不匹配 | 检查 imports 对象 |
RuntimeError | 内存越界、除零、trap | 调试 Wasm 代码 |
RangeError | 内存分配失败 | 增大 maximum |
| CORS 错误 | 跨域加载 | 配置 CORS 头 |
8.8 Feature Detection
// 检测 Wasm 支持
function hasWasmSupport() {
try {
if (typeof WebAssembly === 'object' &&
typeof WebAssembly.instantiate === 'function') {
const module = new WebAssembly.Module(
new Uint8Array([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00])
);
return module instanceof WebAssembly.Module;
}
} catch (e) {}
return false;
}
// 检测特定特性
function hasWasmSIMD() {
try {
const bytes = new Uint8Array([
0,97,115,109,1,0,0,0,1,5,1,96,0,1,123,3,2,1,0,
10,10,1,8,0,65,0,253,15,253,98,11
]);
return WebAssembly.validate(bytes);
} catch (e) {
return false;
}
}
function hasWasmThreads() {
try {
const bytes = new Uint8Array([
0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,
5,4,1,3,1,1,10,11,1,9,0,254,3,0,2,7,0,11
]);
return WebAssembly.validate(bytes) && typeof SharedArrayBuffer !== 'undefined';
} catch (e) {
return false;
}
}
// 渐进增强策略
async function loadBestModule() {
if (hasWasmSIMD()) {
return loadModule('module-simd.wasm');
} else if (hasWasmSupport()) {
return loadModule('module.wasm');
} else {
return loadFallbackJS();
}
}
8.9 实用工具函数
// wasm-utils.js
// 通用 Wasm 加载器
export async function loadWasm(wasmUrl, imports = {}) {
if (typeof WebAssembly.instantiateStreaming === 'function') {
const response = await fetch(wasmUrl);
const { instance } = await WebAssembly.instantiateStreaming(response, imports);
return instance.exports;
}
const response = await fetch(wasmUrl);
const buffer = await response.arrayBuffer();
const { instance } = await WebAssembly.instantiate(buffer, imports);
return instance.exports;
}
// Wasm 内存管理包装器
export class WasmMemory {
constructor(memory, exports) {
this.memory = memory;
this.exports = exports;
}
get buffer() { return this.memory.buffer; }
writeU8(offset, data) {
new Uint8Array(this.buffer).set(data, offset);
}
readU8(offset, length) {
return new Uint8Array(this.buffer, offset, length).slice();
}
writeString(str, offset) {
const encoded = new TextEncoder().encode(str + '\0');
this.writeU8(offset, encoded);
return offset;
}
readString(offset) {
const view = new Uint8Array(this.buffer);
let end = offset;
while (view[end] !== 0) end++;
return new TextDecoder().decode(view.slice(offset, end));
}
writeF64Array(arr, offset) {
new Float64Array(this.buffer).set(arr, offset / 8);
}
readF64Array(offset, length) {
return new Float64Array(this.buffer, offset / 8, length).slice();
}
}
8.10 注意事项
⚠️ MIME 类型:服务器必须为
.wasm文件返回application/wasm,否则流式编译会失败。
⚠️ SharedArrayBuffer 限制:Chrome 要求
Cross-Origin-Opener-Policy: same-origin和Cross-Origin-Embedder-Policy: require-corp头才能使用SharedArrayBuffer。
⚠️ 内存视图失效:每次 Wasm 内存增长后,所有现有的
TypedArray视图都变成无效的。务必重新创建。
⚠️ i64 兼容性:旧版 Safari 和某些环境不支持 BigInt,需要将 i64 拆分为两个 i32 传递。
8.11 扩展阅读
- MDN: WebAssembly JavaScript API
- V8 Blog: Wasm compilation pipeline
- web.dev: Using WebAssembly
- Chrome DevTools Wasm debugging
下一章:09 - WASI 系统接口 — 让 Wasm 模块安全地访问操作系统功能。