WebAssembly 入门指南:让浏览器跑得更快
什么是 WebAssembly?
WebAssembly(简称 WASM)是一种可以在浏览器中高效运行的二进制指令格式。它不是用来替代 JavaScript 的,而是作为 JavaScript 的补充,让你可以把计算密集型的工作交给 WASM 来处理。
简单来说:WASM 是一个编译目标——你可以用 C、C++、Rust、Go 等语言编写代码,然后编译成 .wasm 文件,在浏览器中以接近原生速度运行。
为什么需要 WASM?
JavaScript 是解释型语言,尽管 JIT 编译器让它越来越快,但在某些场景下仍然不够用:
- 游戏引擎:物理模拟、碰撞检测需要大量计算
- 图像/视频处理:逐像素处理高分辨率媒体
- 密码学:加密解密需要高性能数学运算
- CAD/3D 建模:复杂几何计算
- 科学计算:数据分析、模拟
WASM 的二进制格式可以更快地被解码和执行,同时它采用静态类型,避免了 JavaScript 中大量的类型检查和运行时优化开销。
WASM 核心概念
在深入代码之前,先了解几个核心概念:
| 概念 | 说明 |
|---|---|
| Module | 编译后的 .wasm 文件,包含函数、内存、全局变量等 |
| Memory | WASM 的线性内存空间,以“页”(64KB)为单位,可通过 ArrayBuffer 与 JS 共享 |
| Table | 函数引用表,用于实现函数指针和动态分发 |
| Type System | WASM 仅支持四种数值类型:i32、i64、f32、f64 |
| Import/Export | Module 可以导入 JS 函数,也可以导出自己的函数给 JS 调用 |
WASM 文本格式(WAT)
WAT(WebAssembly Text Format)是 WASM 的可读文本表示。下面是一个最简单的例子——它将两个 i32 整数相加并返回结果:
(module
(func (export "add") (param i32 i32) (result i32)
local.get 0 ;; 获取第一个参数
local.get 1 ;; 获取第二个参数
i32.add)) ;; 执行整数加法
这个 (module) 定义了一个 WASM 模块,里面有:
- 一个函数,接收两个
i32参数,返回一个i32 - 用
(export "add")把这个函数导出,名字叫add - 函数体用
local.get读取参数,i32.add执行加法
交互示例
下面每个示例都是可在线运行的。点击“运行”按钮,你就能看到 WASM 和 JavaScript 如何协作。
示例 1:最简单的 WASM 函数
示例 1:两数相加
(module
(func (export "add") (param i32 i32) (result i32)
local.get 0
local.get 1
i32.add))// 调用 WASM 导出的 add 函数
const a = 42;
const b = 58;
const result = exports.add(a, b);
log(a + " + " + b + " = " + result);
// 再测几组
log("100 + 200 = " + exports.add(100, 200));
log("999 + 1 = " + exports.add(999, 1));这个例子展示了最基本的 WASM 用法:
- 浏览器通过
WebAssembly.instantiate()加载 WASM 二进制 - 从
instance.exports获取导出的add函数 - 像调用普通 JavaScript 函数一样调用它
WASM 会进行类型检查——如果你传入字符串或其他类型,调用会直接失败。
示例 2:单参数运算
示例 2:计算平方
(module
(func (export "square") (param i32) (result i32)
local.get 0
local.get 0
i32.mul))// 计算几个数的平方
for (let i = 1; i <= 10; i++) {
log(i + "² = " + exports.square(i));
}
// WASM 函数可以被赋值、传参,和普通函数一样
const sq = exports.square;
log("\n25² = " + sq(25));i32.mul 是 WASM 的乘法指令。WASM 使用栈式虚拟机模型——操作数先入栈,指令再消费栈顶的值。local.get 0 会把同一个参数入栈两次,i32.mul 把它们相乘。
示例 3:共享内存
示例 3:读写 WASM 内存
(module
(memory (export "memory") 1) ;; 1 页 = 64KB
(func (export "store") (param i32 i32)
local.get 0 ;; 地址
local.get 1 ;; 值
i32.store) ;; 将 32 位整数写入内存
(func (export "load") (param i32) (result i32)
local.get 0 ;; 地址
i32.load)) ;; 从内存读取 32 位整数// WASM 的线性内存以 ArrayBuffer 形式暴露给 JS
const memory = exports.memory;
// 创建一个 Uint32Array 视图来读写 32 位整数
const view = new Uint32Array(memory.buffer);
// 用 WASM 的 store 函数写入
exports.store(0, 42); // 地址 0
exports.store(4, 100); // 地址 4 (每个 i32 占 4 字节)
exports.store(8, 256); // 地址 8
// 用 WASM 的 load 函数读取
log("地址 0: " + exports.load(0));
log("地址 4: " + exports.load(4));
log("地址 8: " + exports.load(8));
// JS 也可以直接读写同一块内存!
view[3] = 999; // 地址 12
log("\nJS 写入地址 12: " + exports.load(12));
log("JS 读取地址 4: " + view[1]);这个例子展示了 WASM 和 JavaScript 共享内存的关键能力:
(memory 1)申请了 1 页(64KB) 线性内存i32.store将 32 位整数写入指定地址i32.load从指定地址读取 32 位整数- JS 通过
exports.memory.buffer获取这块内存的ArrayBuffer,直接读写
这意味着你可以在 WASM 中处理大量数据(如图像像素),然后让 JS 读取结果来渲染。零拷贝,非常高效。
示例 4:性能对比 — WASM vs JS vs Worker
这个例子对比三种方式执行相同计算密集型任务(3000 万次循环累加)的耗时:
- WASM:二进制指令直接在浏览器虚拟机中执行,接近原生速度
- 纯 JS:JIT 编译,有类型检查和优化开销
- Web Worker:在后台线程执行,不阻塞主线程,但执行速度与纯 JS 相同
示例 4:性能对比
(module
(func (export "compute") (param i32) (result i32)
(local i32)
i32.const 0
local.set 1 ;; sum = 0
loop $loop
local.get 1
local.get 0
i32.add ;; sum += n
local.set 1
local.get 0
i32.const 1
i32.sub ;; n--
local.tee 0
i32.const 0
i32.gt_s ;; n > 0 ?
br_if $loop ;; 是则继续循环
end
local.get 1))const ITERATIONS = 30000000;
// 辅助:纯 JS 版本
function jsCompute(n) {
var sum = 0;
for (var i = n; i > 0; i--) sum += i;
return sum;
}
// 1. WASM 测试
log("=== 累加 " + ITERATIONS.toLocaleString() + " 次循环 ===");
var wasmStart = performance.now();
var wasmResult = exports.compute(ITERATIONS);
var wasmTime = performance.now() - wasmStart;
log("WASM : " + wasmTime.toFixed(1) + "ms (结果: " + wasmResult + ")");
// 2. 纯 JS 测试
var jsStart = performance.now();
var jsResult = jsCompute(ITERATIONS);
var jsTime = performance.now() - jsStart;
log("纯 JS : " + jsTime.toFixed(1) + "ms (结果: " + jsResult + ")");
log("\nWASM 比纯 JS 快 " + (jsTime / wasmTime).toFixed(2) + "x");
// 3. Web Worker 测试(异步,不阻塞主线程)
return new Promise(function(resolve) {
var workerCode = 'self.onmessage=function(e){var n=e.data,s=0;for(var i=n;i>0;i--)s+=i;self.postMessage(s)}';
var blob = new Blob([workerCode],{type:"application/javascript"});
var worker = new Worker(URL.createObjectURL(blob));
var workerStart = performance.now();
worker.postMessage(ITERATIONS);
worker.onmessage = function(e) {
var workerTime = performance.now() - workerStart;
log("Worker: " + workerTime.toFixed(1) + "ms (结果: " + e.data + ")");
log("Worker 速度与纯 JS 相近,但计算在后台线程,\n不阻塞主线程,页面保持响应。");
resolve();
};
});WASM 为什么更快?
- 预编译:WASM 在
instantiate()阶段就已经完成编译,执行时无需 JIT 预热 - 静态类型:WASM 指令直接操作
i32/f64等固定类型,无需运行时类型检查 - 无 GC:线性内存无需垃圾回收,没有 GC 暂停
- 紧凑指令集:二进制格式更小,解码更快
Worker 的价值不在于速度,而在于不阻塞主线程。 对于需要保持 UI 流畅的场景(如动画、输入响应),把重计算放到 Worker 中是更好的选择。Worker + WASM 的组合(在 Worker 中加载 WASM)则兼具两者优势。
在 JS 中加载 WASM
加载 WASM 有多种方式,取决于你的项目环境。
方式 1:原生 fetch(纯 JS/TS,无需构建工具)
最灵活、最通用的方式,在任意 JS 运行环境中都能用:
// 基本用法
const response = await fetch("/path/to/module.wasm");
const buffer = await response.arrayBuffer();
const { instance } = await WebAssembly.instantiate(buffer);
// 调用导出的函数
console.log(instance.exports.add(1, 2));
带 importObject 让 WASM 调用 JS 函数:
const importObject = {
env: {
// 让 WASM 可以调用 console.log
log_number: (n) => console.log("WASM says:", n),
// 提供三角函数等 WASM 没有的内置函数
sin: Math.sin,
},
};
const response = await fetch("/module.wasm");
const buffer = await response.arrayBuffer();
const { instance } = await WebAssembly.instantiate(buffer, importObject);
importObject 允许 WASM 模块调用 JavaScript 函数,这是双向通信的基础。
如果 WASM 操作了共享内存,通过 instance.exports.memory 获取:
const memory = instance.exports.memory;
// JS 可以读写同一块 ArrayBuffer
const view = new Uint32Array(memory.buffer);
view[0] = 42;
方式 2:Vite 构建工具
Vite 对 .wasm 文件有原生支持,你可以像导入模块一样导入它——Vite 自动处理 fetch、编译和导出绑定:
// 默认导出是初始化函数,具名导出是 WASM 函数
import init, { add, fibonacci } from "./math.wasm";
await init(); // 必须先初始化
console.log(add(1, 2)); // 之后才能调用
init 是 Vite 自动生成的初始化函数(负责 fetch + instantiate),add / fibonacci 是 WASM 模块中 (export "add" ...) 的函数。必须 await init() 后才可调用。
如果不想阻塞模块加载,可以用异步 import:
const { default: init, add } = await import("./math.wasm");
await init();
方式 3:Webpack 构建工具
Webpack 4+ 支持 WASM,但需要开启实验特性。在 webpack.config.js 中:
module.exports = {
experiments: {
asyncWebAssembly: true, // 启用异步 WASM
// syncWebAssembly: true, // 同步模式(不推荐,会阻塞)
},
};
然后在代码中直接 import:
// 异步导入(推荐)
import("./math.wasm").then(({ add }) => {
console.log(add(1, 2));
});
// 顶层 await 也可以
const { add } = await import("./math.wasm");
注意 Webpack 的 WASM 文件大小会被限制(默认 4KB 以内内联),更大的文件会自动走异步 chunk 加载。
方式 4:Vite/Webpack 中配合 Worker
在构建工具中,可以把 WASM 和 Worker 结合起来,同时享受工具链和后台线程的优势:
// worker.js
import init, { compute } from "./heavy.wasm";
self.onmessage = async (e) => {
await init();
const result = compute(e.data);
self.postMessage(result);
};
// 主线程
const worker = new Worker(new URL("./worker.js", import.meta.url));
worker.postMessage(30000000);
worker.onmessage = (e) => console.log("结果:", e.data);
Vite 的 new URL(..., import.meta.url) 和 Webpack 的 new Worker(...) 都会自动处理 Worker 和 WASM 的打包。
编译语言到 WASM
在实际项目中,你通常不会手写 WAT。以下是主流编译方案:
Rust → WASM
# 使用 wasm-pack
cargo install wasm-pack
wasm-pack build --target web
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn fibonacci(n: u32) -> u32 {
match n {
0 => 0,
1 => 1,
_ => fibonacci(n - 1) + fibonacci(n - 2),
}
}
C/C++ → WASM
# 使用 Emscripten
emcc hello.c -o hello.js -s WASM=1
#include <emscripten.h>
EMSCRIPTEN_KEEPALIVE
int add(int a, int b) {
return a + b;
}
AssemblyScript
AssemblyScript 是 TypeScript 的子集,专门编译到 WASM:
// AssemblyScript 代码
export function add(a: i32, b: i32): i32 {
return a + b;
}
npx asc add.ts -o add.wasm --optimize
WASM 的限制
虽然 WASM 很强大,但它也有一些限制需要了解:
- 只能操作数值:WASM 不能直接操作 DOM、不能发起网络请求——这些都需要通过调用导入的 JS 函数来完成
- 不支持垃圾回收:线性内存需要手动管理(不过 GC 提案正在推进中)
- 类型有限:仅支持
i32、i64、f32、f64。复杂类型(字符串、对象)需要编码为内存中的字节 - 调试困难:相比 JS,WASM 的调试工具还不够成熟
什么时候用 WASM?
WASM 并不是“更好的 JavaScript”。把整个应用都编译成 WASM 通常没有意义。它最适合用在:
- 识别出性能瓶颈在计算密集的部分
- 有现成的 C/Rust 库想在浏览器中复用
- 需要跨语言复用同一套计算逻辑(前端 + 后端 + CLI)
常见场景:图像编辑器(Figma)、游戏引擎(Unity WebGL)、代码编辑器、CAD 查看器等。