Chapter 01

WebAssembly 概念:二进制格式与运行时

从 asm.js 的历史局限出发,理解 WebAssembly 的设计理念、安全模型与浏览器支持现状

从 asm.js 到 WebAssembly

JavaScript 的性能瓶颈

JavaScript 诞生于 1995 年,最初只是用于网页表单验证的简单脚本语言。随着 Web 应用越来越复杂,工程师们开始挑战 JS 的性能极限:游戏、3D 渲染、音视频编解码、密码学计算……这些计算密集型任务在 JS 中表现糟糕。

即使经过 V8 引擎的 JIT(即时编译)优化,JavaScript 由于其动态类型特性,引擎在运行时需要频繁检查变量类型、可能触发垃圾回收暂停,在纯数值计算上的性能往往只有原生 C 代码的 1/3 到 1/5

asm.js:WebAssembly 的前身

asm.js 是 Mozilla 于 2013 年发布的技术,这是 WebAssembly 的直接前身。它的思路是:用 JavaScript 的一个严格子集来写代码,这个子集全部使用静态类型(通过位运算等技巧提示类型),让 JS 引擎可以直接编译为机器码,跳过动态类型检查。

// asm.js 风格的代码(由 Emscripten 生成,人类通常不手写)
function AsmModule(stdlib, foreign, heap) {
  "use asm";  // 声明这是 asm.js 模块
  var imul = stdlib.Math.imul;

  // 所有变量都有明确的类型注解(通过 |0 表示 int,+x 表示 double)
  function add(x, y) {
    x = x | 0;    // 强制转为 32位整数
    y = y | 0;
    return (x + y) | 0;
  }

  return { add: add };
}

asm.js 的主要限制:

WebAssembly 的诞生(2015-2019)

2015 年,Mozilla、Google、Microsoft、Apple 四家主要浏览器厂商史无前例地联合宣布合作开发 WebAssembly。2017 年各大浏览器同步发布了初版支持,2019 年 W3C 正式将 WebAssembly 发布为 Web 标准——这是继 HTML、CSS、JavaScript 之后第四个 W3C Web 标准语言格式。

2015 年 6 月
四大浏览器厂商联合宣布 WebAssembly 项目,以 asm.js 为参考进行设计。
2016 年
各浏览器 Nightly/Canary 版本开始提供实验性支持,开发者社区开始试用。
2017 年 2 月
Chrome 57、Firefox 52、Edge 15、Safari 11 相继发布稳定版 Wasm 支持,四大浏览器首次同步支持新特性。
2019 年 12 月
W3C 将 WebAssembly 发布为正式推荐标准(W3C Recommendation),成为 Web 第四语言。
2022 年
WASI Preview 1 发布,WasmGC 提案进入浏览器,支持垃圾回收语言(Java/Kotlin/Go)直接编译到 Wasm。
2024 年
Wasm 组件模型(Component Model)标准化推进,WASI 0.2 发布,生态系统日趋成熟。

WebAssembly 的二进制格式

.wasm 文件结构

WebAssembly 有两种等价的表示格式:二进制格式(.wasm 文件)和文本格式(.wat 文件,WebAssembly Text)。就像机器码和汇编语言的关系,两者可以无损互转,但浏览器执行的是二进制格式。

┌─────────────────────────────────────────────────────────┐ │ .wasm 二进制文件结构 │ ├──────────────────────────────────────────────────────────┤ │ Magic Number: 0x00 0x61 0x73 0x6D (即 "\0asm") │ │ Version: 0x01 0x00 0x00 0x00 (版本 1) │ ├──────────────────────────────────────────────────────────┤ │ Section 1: Type Section (函数签名定义) │ │ Section 2: Import Section (导入的函数/内存/全局变量) │ │ Section 3: Function Section (函数索引→类型映射) │ │ Section 4: Table Section (函数引用表) │ │ Section 5: Memory Section (线性内存声明) │ │ Section 6: Global Section (全局变量) │ │ Section 7: Export Section (导出到 JS 的接口) │ │ Section 8: Start Section (模块初始化入口) │ │ Section 9: Element Section (表的初始化) │ │ Section 10: Code Section (函数体字节码) │ │ Section 11: Data Section (内存初始数据) │ └──────────────────────────────────────────────────────────┘

二进制 vs 文本格式对比

;; .wat 文本格式 — 一个简单的加法函数
(module
  (func $add (param $a i32) (param $b i32) (result i32)
    local.get $a
    local.get $b
    i32.add
  )
  (export "add" (func $add))
)
;; 对应的二进制格式(十六进制表示)
00 61 73 6d  ; 魔数 "\0asm"
01 00 00 00  ; 版本 1
01 07        ; Type section(长度 7 字节)
01 60 02 7f  ; 1 个函数类型: (i32, i32)
7f 01 7f     ; -> i32
03 02 01 00  ; Function section: 函数 0 使用类型 0
07 07 01 03  ; Export section: 导出 "add"
61 64 64 00  ; "add" 对应函数 0
0a 09 01 07  ; Code section
00 20 00 20  ; local.get 0, local.get 1
01 6a 0b     ; i32.add, end
为什么二进制格式如此重要?

与 JavaScript 文本格式相比,.wasm 二进制格式的优势在于:
1. 体积更小:二进制编码比等效 JS/WAT 文本小 3-5 倍
2. 解析极快:浏览器可以直接将字节码编译为机器码,解析速度比 JS 快 10-20 倍
3. 流式编译:使用 WebAssembly.instantiateStreaming() 可以在下载的同时进行编译,无需等待完整下载

WebAssembly 的安全模型

线性内存沙箱

WebAssembly 最重要的安全保证来自其线性内存模型。每个 Wasm 模块只能访问一块连续的字节数组(线性内存),这块内存与 JS 的堆内存完全隔离,也与宿主系统的内存隔离。Wasm 代码无法越界访问这块内存之外的任何数据。

浏览器内存空间 ┌──────────────────────────────────────────────────────┐ │ JavaScript Heap(V8 堆) │ │ ┌────────────────┐ ┌─────────────────────────┐ │ │ │ JS 对象/数组 │ │ Wasm 线性内存 │ │ │ │ JS 闭包/函数 │ │ [0, 1, 2, ..., N字节] │ │ │ │ │ │ Wasm 代码只能访问这里 │ │ │ └────────────────┘ └─────────────────────────┘ │ │ │ │ Wasm 代码无法直接访问 JS 对象 │ │ Wasm 代码无法访问系统内存/其他进程 │ └──────────────────────────────────────────────────────┘

安全特性总结

内存隔离
Wasm 模块只能访问被分配的线性内存,无法读写 JS 堆或宿主系统内存,防止任意内存读写攻击。
控制流完整性
Wasm 的控制流(函数调用、循环、分支)在验证阶段由引擎静态验证,不能执行任意代码跳转,防御 ROP(Return-Oriented Programming)攻击。
类型安全
所有函数签名和指令操作数类型在加载时进行静态验证,类型不匹配直接拒绝执行,不会在运行时崩溃。
能力限制
Wasm 模块默认没有任何系统访问能力(不能读文件、不能发网络请求),所有外部能力必须通过 JS 或 WASI 显式导入。
沙箱执行
运行在与网页同等的安全沙箱中,受 Same-Origin Policy、Content Security Policy 等 Web 安全机制约束。

Wasm 与 JavaScript 的关系

互补,而非替代

一个常见的误解是:WebAssembly 会取代 JavaScript。这个观点是错误的。Wasm 和 JS 是互补关系,各有其擅长领域:

维度 JavaScript WebAssembly
类型系统 动态类型 静态类型(i32/i64/f32/f64)
DOM 访问 原生支持 必须通过 JS 桥接
数值计算 中等 接近原生,快 5-20x
启动速度 快(小文件) 中等(编译开销)
垃圾回收 自动 GC 手动/WasmGC
调试体验 优秀 中等(需 DWARF 符号)
最适合 UI 逻辑、DOM 操作、胶水代码 计算密集型:编解码、加密、物理引擎、图像处理

典型的 Wasm + JS 协作模式

// 典型架构:JS 负责 UI 和胶水代码,Wasm 负责计算
async function processImage(imageData) {
  // 1. 加载 Wasm 模块
  const { instance } = await WebAssembly.instantiateStreaming(
    fetch('image_processor.wasm')
  );

  // 2. 将图像数据写入 Wasm 线性内存
  const wasmMemory = new Uint8Array(instance.exports.memory.buffer);
  wasmMemory.set(imageData, 0);

  // 3. 调用 Wasm 函数执行计算(CPU 密集型)
  const resultOffset = instance.exports.apply_gaussian_blur(
    0,              // 输入数据偏移
    imageData.length, // 数据长度
    5               // 模糊半径
  );

  // 4. 从 Wasm 内存读取结果,交回 JS 处理
  return new Uint8Array(
    instance.exports.memory.buffer,
    resultOffset,
    imageData.length
  );
}

浏览器运行时支持

V8(Chrome/Node.js/Deno)

Google V8 引擎对 Wasm 的支持最为领先,它实现了 Liftoff(快速基线编译器,减少启动延迟)和 Turbofan(优化编译器,提升执行性能)两级 JIT 编译:

SpiderMonkey(Firefox)

Mozilla Firefox 的 SpiderMonkey 是 Wasm 最早的原型验证平台,对 Wasm 规范贡献最多。它同样实现了两级编译(Cranelift 基线 + Ion 优化),对 WASI 和安全沙箱的支持尤为严格。

JavaScriptCore(Safari/WebKit)

苹果的 JSC 对 Wasm 的支持相对保守,新提案(如 SIMD、线程)的实现时间线通常落后于 V8 和 SpiderMonkey 6-12 个月。开发 iOS/macOS Web 应用时需特别注意兼容性。

检查浏览器 Wasm 特性支持

可以使用 WebAssembly.validate() 检测基础 Wasm 支持,使用 WebAssembly.SIMDcrossOriginIsolated(线程/SharedArrayBuffer)等检测高级特性。webassembly.org/features 提供完整的特性支持矩阵。

常见误区与注意事项

误区一:WebAssembly 会取代 JavaScript

这是最常见的误解。Wasm 和 JS 是互补关系,不是竞争关系。JS 仍然是控制 DOM、处理事件和编写业务逻辑的最佳选择。Wasm 只在计算密集型场景(密码学、图像处理、物理引擎)才有明显优势。甚至 Wasm 本身访问 DOM 还需要通过 JS 桥接。

误区二:Wasm 总是比 JavaScript 快

这也是错误的。对于以下场景,JS 可能与 Wasm 性能相当甚至更好:

Wasm 的优势集中在:大规模数值计算、SIMD 向量运算、移植现有 C/C++/Rust 库。

误区三:Wasm 不安全,因为可以执行机器码

恰恰相反,Wasm 的安全模型比 JS 更严格。Wasm 运行在沙箱中,所有内存访问都被边界检查,控制流被静态验证,没有任何默认的系统访问权限。Wasm 无法像 JS 那样通过 eval() 动态执行任意代码。

内存增长后 TypedArray 视图失效

这是实际开发中最常见的 Bug。当 Wasm 内存通过 memory.grow() 扩展时,所有基于该内存的 TypedArray 视图都会失效(底层 ArrayBuffer 被替换)。你必须在每次内存可能增长后重新获取 TypedArray 视图:

const { instance } = await WebAssembly.instantiate(wasmBytes);
let mem = new Uint8Array(instance.exports.memory.buffer);

instance.exports.grow_memory();  // Wasm 内部调用 memory.grow()

// ❌ 错误:mem 现在指向旧的 ArrayBuffer,已失效
// mem[0] = 42; // 不会写入当前内存!

// ✅ 正确:内存增长后重新获取视图
mem = new Uint8Array(instance.exports.memory.buffer);
mem[0] = 42;  // 现在正确写入

WebAssembly 的现实应用案例

已在生产中使用 Wasm 的知名项目

Figma
设计工具 Figma 的渲染引擎使用 WebAssembly(基于 C++ 编译)。使用 Wasm 的原因是将原有的桌面渲染引擎移植到 Web 端,性能接近原生应用。
Google Earth Web
Google Earth 的 Web 版本大量使用 WebAssembly 来运行 3D 渲染代码,使浏览器端能够流畅展示地球三维视图。
FFmpeg.wasm
将 FFmpeg 音视频处理库完整编译为 WebAssembly,在浏览器端实现视频转码、剪辑等功能,无需服务器参与。
SQLite(wa-sqlite)
SQLite 数据库编译为 Wasm,在浏览器中运行,可以将持久化数据存储在 IndexedDB 中,实现真正的客户端数据库。
Pyodide
将 CPython 解释器编译为 WebAssembly,允许在浏览器中运行 Python 代码(包括 NumPy、Pandas、SciPy 等科学计算库)。这是 JupyterLite 的核心技术。
Shopify
使用 WebAssembly(配合 WASI)在 Cloudflare Workers 边缘节点运行商家自定义逻辑,每次请求都在沙箱中安全执行用户代码。

本章小结

本章核心要点