WebAssembly 基础
问题
什么是 WebAssembly?它与 JavaScript 的关系是什么?在前端有哪些应用场景?
答案
1. 什么是 WebAssembly
WebAssembly(简称 Wasm)是一种低级的二进制格式,设计为高性能的编译目标。它不是用来替代 JavaScript 的,而是与 JS 互补。
| 特点 | 说明 |
|---|---|
| 二进制格式 | 紧凑、解析快(比 JS 文本解析快 10-20 倍) |
| 接近原生性能 | 编译后的代码运行速度接近 C/C++ |
| 类型安全 | 强类型(i32、i64、f32、f64)、内存安全 |
| 沙箱运行 | 在安全沙箱中执行,无法直接访问 DOM 或系统 |
| 可移植 | 与平台无关的字节码,所有主流浏览器支持 |
| 语言无关 | C/C++、Rust、Go、AssemblyScript 等都可编译为 Wasm |
2. Wasm 与 JavaScript 的关系
WebAssembly 不是用来替代 JavaScript 的。它更像是 JavaScript 的"协处理器",专门用于 计算密集型 任务。
| 维度 | JavaScript | WebAssembly |
|---|---|---|
| 语法 | 高级脚本语言 | 低级二进制格式 |
| 类型 | 动态类型 | 静态强类型 |
| 解析速度 | 需要解析文本 | 二进制直接解码,极快 |
| 执行速度 | JIT 优化后较快,但有瓶颈 | 接近原生 |
| DOM 操作 | 直接操作 | 不能直接操作,需通过 JS |
| GC | 有垃圾回收 | 手动管理内存(Wasm GC 提案中) |
| 适用场景 | UI 逻辑、DOM 操作、业务代码 | 计算密集、图形处理、编解码 |
| 开发体验 | 生态丰富、调试方便 | 需编译工具链 |
3. Wasm 模块结构
Wasm 有两种表示格式:
- WAT(WebAssembly Text Format):可读的文本格式
- WASM(WebAssembly Binary Format):二进制格式
;; WAT 格式 - 一个简单的加法函数
(module
;; 导出函数 "add"
(func $add (export "add") (param $a i32) (param $b i32) (result i32)
local.get $a
local.get $b
i32.add
)
;; 导出内存
(memory (export "memory") 1) ;; 1 页 = 64KB
)
数据类型
Wasm 只有 4 种基本类型:
| 类型 | 说明 | 对应 |
|---|---|---|
i32 | 32 位整数 | int / boolean |
i64 | 64 位整数 | long |
f32 | 32 位浮点数 | float |
f64 | 64 位浮点数 | double |
Wasm 没有原生字符串类型。字符串需要通过线性内存(Linear Memory)以字节数组的形式在 JS 和 Wasm 之间传递。
4. JavaScript 与 Wasm 互操作
加载和实例化 Wasm 模块
// 方式 1: WebAssembly.instantiateStreaming(推荐,流式编译)
async function loadWasm(): Promise<WebAssembly.Instance> {
const response = await fetch('/math.wasm');
const { instance } = await WebAssembly.instantiateStreaming(response, {
// 导入对象:JS 函数供 Wasm 调用
env: {
log: (value: number) => console.log('Wasm says:', value),
memory: new WebAssembly.Memory({ initial: 1 }), // 1 页 = 64KB
},
});
return instance;
}
// 方式 2: 先获取 ArrayBuffer 再编译
async function loadWasm2(): Promise<WebAssembly.Instance> {
const response = await fetch('/math.wasm');
const buffer = await response.arrayBuffer();
const module = await WebAssembly.compile(buffer);
const instance = await WebAssembly.instantiate(module, { env: {} });
return instance;
}
// 使用导出的函数
const instance = await loadWasm();
const exports = instance.exports as { add: (a: number, b: number) => number };
console.log(exports.add(1, 2)); // 3
内存交互
Wasm 使用线性内存(Linear Memory),JS 和 Wasm 共享同一块 ArrayBuffer:
// Wasm 导出的内存
const memory = instance.exports.memory as WebAssembly.Memory;
// 通过 TypedArray 读写内存
const view = new Uint8Array(memory.buffer);
// 传递字符串到 Wasm
function passStringToWasm(str: string, offset: number): number {
const encoder = new TextEncoder();
const bytes = encoder.encode(str);
const wasmMemory = new Uint8Array(memory.buffer);
wasmMemory.set(bytes, offset);
return bytes.length;
}
// 从 Wasm 读取字符串
function readStringFromWasm(offset: number, length: number): string {
const decoder = new TextDecoder();
const bytes = new Uint8Array(memory.buffer, offset, length);
return decoder.decode(bytes);
}
// 传递数组
function passArrayToWasm(arr: Float64Array, offset: number): void {
const wasmView = new Float64Array(memory.buffer, offset, arr.length);
wasmView.set(arr);
}
5. 编译工具链
Emscripten(C/C++ → Wasm)
Emscripten 是最成熟的 C/C++ → Wasm 编译工具链:
// math.c
#include <emscripten.h>
EMSCRIPTEN_KEEPALIVE
int fibonacci(int n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
# 编译
emcc math.c -o math.js -s EXPORTED_FUNCTIONS='["_fibonacci"]' -s MODULARIZE=1
// 使用 Emscripten 编译产物
import createModule from './math.js';
const Module = await createModule();
console.log(Module._fibonacci(40)); // 比纯 JS 快数倍
wasm-pack(Rust → Wasm)
wasm-pack 是 Rust → Wasm 的工具链,配合 wasm-bindgen 自动生成 JS 绑定:
// src/lib.rs
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn fibonacci(n: u32) -> u32 {
match n {
0 | 1 => n,
_ => fibonacci(n - 1) + fibonacci(n - 2),
}
}
#[wasm_bindgen]
pub fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
wasm-pack build --target web
import init, { fibonacci, greet } from './pkg/my_wasm.js';
await init(); // 初始化 Wasm 模块
console.log(fibonacci(40)); // 计算密集型任务
console.log(greet('World')); // 字符串自动转换
AssemblyScript(TypeScript-like → Wasm)
AssemblyScript 使用类 TypeScript 语法,前端开发者最友好:
// assembly/index.ts(AssemblyScript,非标准 TS)
export function fibonacci(n: i32): i32 {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
export function sum(arr: Int32Array): i32 {
let total: i32 = 0;
for (let i = 0; i < arr.length; i++) {
total += unchecked(arr[i]); // unchecked 跳过边界检查,更快
}
return total;
}
npx asc assembly/index.ts --outFile build/optimized.wasm --optimize
6. WASI — WebAssembly System Interface
WASI 让 Wasm 在浏览器之外运行(Node.js、Deno、独立运行时),提供标准化的系统接口:
WASI 的应用场景:
- Serverless / 边缘计算:冷启动快、安全隔离(Cloudflare Workers、Vercel Edge)
- 插件系统:安全的第三方代码执行
- 跨平台 CLI:一次编译,到处运行
7. Wasm 的局限性
| 局限 | 说明 | 解决方案 |
|---|---|---|
| 无法直接操作 DOM | Wasm 运行在沙箱中 | 通过 JS 桥接 |
| 无 GC | 手动管理内存 | Wasm GC 提案、使用 Rust 的所有权系统 |
| 字符串传递开销 | 需要通过内存拷贝 | wasm-bindgen 自动处理 |
| 调试困难 | 二进制格式不可读 | Source Map、DWARF 调试信息 |
| 包体积 | .wasm 文件可能较大 | wasm-opt 优化、Brotli 压缩 |
| 加载时间 | 首次编译有开销 | 流式编译 + 缓存 |
常见面试问题
Q1: WebAssembly 比 JavaScript 快在哪里?
答案:
| 阶段 | JavaScript | WebAssembly |
|---|---|---|
| 解析 | 文本→AST→字节码,耗时 | 二进制直接解码,快 10-20x |
| 编译 | JIT 分层编译,有预热期 | AOT/流式编译,启动即快 |
| 执行 | 动态类型,需类型检查 | 静态类型,直接执行 |
| 优化 | JIT 可能去优化(deopt) | 编译时已优化,无 deopt |
| GC | GC 暂停(虽短但存在) | 无 GC 暂停 |
| 内存布局 | 对象分散在堆中 | 线性内存,缓存友好 |
但 Wasm 不是在所有场景都快:
- DOM 操作:JS 更快(Wasm 需要跨边界调用)
- 短小函数:JS JIT 优化后差距不大
- I/O 密集:瓶颈在 I/O,不在计算
Q2: WebAssembly 能替代 JavaScript 吗?
答案:
不能,也不应该。设计上它们是互补关系:
- JS 的强项:DOM 操作、事件处理、UI 逻辑、快速原型、丰富生态
- Wasm 的强项:数值计算、图形处理、编解码、密码学、物理模拟
最佳模式是 JS 为主、Wasm 补充:用 JS 处理 UI 和业务逻辑,将计算密集的热点用 Wasm 实现。
Q3: 如何在项目中引入 WebAssembly?
答案:
引入路径从低到高:
-
使用现有 Wasm 库(最简单)
// 例如 sql.js(SQLite 的 Wasm 版)
import initSqlJs from 'sql.js';
const SQL = await initSqlJs();
const db = new SQL.Database(); -
AssemblyScript(前端友好)
- 类 TypeScript 语法,学习曲线低
- 适合简单的计算函数
-
Rust + wasm-pack(推荐,性能+安全)
- 零成本抽象、内存安全
- wasm-bindgen 自动生成 JS 绑定
-
C/C++ + Emscripten(移植老项目)
- 适合将已有 C/C++ 库移植到 Web
Q4: Wasm 的线性内存模型是什么?
答案:
Wasm 使用一块连续的 ArrayBuffer 作为内存,称为线性内存(Linear Memory):
// 创建 1 页(64KB)内存
const memory = new WebAssembly.Memory({ initial: 1, maximum: 10 });
// JS 侧通过 TypedArray 视图读写
const buffer = new Int32Array(memory.buffer);
buffer[0] = 42;
// Wasm 侧通过指针访问同一块内存
// (i32.load (i32.const 0)) → 42
- 单位是 页(Page),1 页 = 64KB
- 可通过
memory.grow(n)动态增长 - JS 和 Wasm 共享同一块 buffer,是主要的数据交换方式
- 内存增长后,原有的
TypedArray视图会失效,需要重新创建
Q5: WebAssembly 在前端有哪些典型应用?
答案:
| 应用 | 代表项目 | 说明 |
|---|---|---|
| 图片/视频处理 | FFmpeg.wasm、Squoosh | 浏览器端编解码 |
| 设计工具 | Figma、AutoCAD Web | 复杂图形渲染 |
| 代码编辑器 | VS Code Web(部分) | 语法高亮、语言服务 |
| 游戏引擎 | Unity WebGL、Godot | 3D 渲染、物理模拟 |
| 数据库 | sql.js、DuckDB-Wasm | 浏览器端 SQL |
| 加密 | libsodium.js | 高性能密码学 |
| PDF.js(部分) | PDF 渲染 | |
| AI 推理 | ONNX Runtime Web | 浏览器端模型推理 |
| 地图 | Google Earth | 3D 地球渲染 |
| Office | Google Sheets | 计算引擎 |
相关链接
- WebAssembly 官网
- MDN WebAssembly 文档
- Emscripten 文档
- wasm-pack
- AssemblyScript
- WASI
- Wasm 实战应用 — FFmpeg.wasm、性能对比、实战案例