跳转至

在浏览器里跑起 WebAssembly — 生命周期、JS API 与线性内存

1. 这一章讲什么

第 2 章把 .wasm 文件造了出来,这一章讲它在浏览器里怎么活过来: 从下载字节开始,到编译、实例化、调函数、读写内存,再到出错怎么办、安全边界在哪。 这一章的机制是全书最常用的一层——后面 Rust、C/C++、Node.js 各章,本质上都是在 不同语言、不同宿主里重复这一套生命周期。1

2. 顶层全景

  fetch 拿到 .wasm 字节流
      │  校验:格式合不合规范(内存、函数导入表齐不齐)
      ▼
  编译:JS 引擎(V8 / SpiderMonkey)把字节码翻成机器码
      │  streaming 模式:字节一边到一边编译,不等下载完
      ▼
  实例化:把 import(JS 函数、Memory 对象)接进模块,分配线性内存
      │
      ▼
  JS 调 exports 里的函数;数据经线性内存 + 类型化数组双向流动

图说:fetch → 编译 → 实例化 → 执行,是每一家浏览器、每一个 Wasm 应用的必经四站。

3. 核心原理

3.1 四站生命周期,每站一句话

原书把浏览器执行 Wasm 拆成四个阶段,每站的产出是下一站的输入:2

站 干什么 关键产物
取(fetch) 用 Fetch API 把 .wasm 当二进制文件取回来 字节流
编译(compile) 引擎校验模块结构,再把字节码翻译成针对本机 CPU 的机器码 编译好的模块
实例化(instantiate) 把模块声明的 import(函数、内存、表)和外界提供的对象接起来,分配线性内存 活的实例
执行(run) JS 直接调实例上 export 出来的函数 计算结果

流式编译(streaming compilation) 是编译这一站的关键优化: 引擎不等整个文件下完,字节流到达一段就解析编译一段,大模块因此能做到「几乎即时启动」。3

两个容易混的概念在这里分清:

  • 编译:只把字节码翻译成机器码,产出一个「模块对象」,还不能跑;
  • 实例化:给这个模块接上 import、铺好内存,产出「实例」——同一个模块可以实例化出多个 互相独立的实例,各有自己的内存。

3.2 两个 API:instantiate 与 instantiateStreaming

JS 侧入口有两个,差别只在字节怎么来:4

WebAssembly.instantiate()(四步链):

fetch('math.wasm')                                    // ① 发 HTTP 请求
  .then(response => response.arrayBuffer())          // ② 等下完,整包转成 ArrayBuffer
  .then(bytes => WebAssembly.instantiate(bytes,      // ③ 编译 + 实例化一步完成
           { env: { log: console.log } }))           //    第二个参数是 import 对象
  .then(result => {
    const { add } = result.instance.exports;         // ④ 从 exports 里取函数
    console.log(add(3, 4)); // 7
  });

注意第三步那个 import 对象:{ env: { log: console.log } } 的意思是—— Wasm 模块里声明「我要从 env 这组里 import 一个叫 log 的函数」, 实例化时由 JS 把 console.log 塞进去。模块从此有了「对外说话」的能力。5

WebAssembly.instantiateStreaming()(一步到位):

WebAssembly.instantiateStreaming(fetch('math.wasm'), { env: { log: console.log } })
  .then(result => {
    const { multiply } = result.instance.exports;
    console.log(multiply(5, 6)); // 30
  });

区别就一处:fetch 的返回值(Response 对象,还没读body)直接交给引擎, 字节一边流一边编译,省掉「等下完再开工」的等待。6 书上给的选用口径:有网络就用 streaming,离线兜底用普通 instantiate。7

3.3 异步与错误处理:Promise 链上每一环都可能断

这些 API 全是异步的(返回 Promise——一个「将来某时刻才有结果」的对象), 链上任何一环断了——网络中断、字节流损坏、import 签名对不上——Promise 就 reject(失败), 程序如果没接住就静默黑屏。书里的做法:8

  • 用 .catch() 接链式调用,或用 async/await 配 try-catch 包一层;
  • import 签名不匹配会抛 TypeError,用 wasm-objdump 查模块到底声明了哪些 import;
  • 优雅降级:Wasm 起不来就退回 JavaScript 实现,页面照常工作。

书里给了一个带兜底的骨架:try { await instantiateStreaming(...) } catch { console.error(...); 换 JS 版 }。9

模块从 CDN 取或经 Webpack 打包也各给了例子——Webpack 要开 experiments: { asyncWebAssembly: true },然后 import wasmModule from './crypto.wasm' 就能像引普通模块一样引 .wasm。10

3.4 线性内存:一页 64KB 的可伸缩字节带

线性内存是一条连续的字节数组,Wasm 和 JavaScript 都能碰。 它按页增长,一页固定 64KB(65 536 字节)。11

new WebAssembly.Memory({ initial: 1, maximum: 10 });  // 初始 1 页,封顶 10 页
memory.grow(2);   // 加 2 页 → 现在 3 页 = 192KB;返回值是增长前的页数(1)

initial 是开局页数,maximum 是封顶(可省略,省略=不设上限)。 grow() 返回增长前的页数——这个反直觉的返回值是 wasm 规范定的,做「记下旧边界」时有用。12

JS 侧怎么读写这条字节带:拿 memory.buffer(一个 ArrayBuffer),在上面开 类型化数组视图。同一个 buffer,Uint8Array 按 1 字节一个格子看, Int32Array 按 4 字节一个格子看——数据不动,只是「看法」不同。13

3.5 跨边界传数据:字符串走一遍就懂

Wasm 只认数,字符串必须编码成字节再进内存。书里有个十二字节的小例子, 把整套传数流程压缩到了最短:14

const memory = new WebAssembly.Memory({ initial: 1 });
const { instance } = await WebAssembly.instantiateStreaming(
  fetch('string.wasm'), { env: { memory } });        // ① 把内存作为 import 交出去
const bytes = new TextEncoder().encode("Hello, Wasm!");  // ② 字符串 → 12 个 UTF-8 字节
const array = new Uint8Array(memory.buffer, 0, bytes.length);
array.set(bytes);                                   // ③ 字节写进内存 0 号格子起
instance.exports.toUpperCase(0, bytes.length);      // ④ 告诉 Wasm:从 0 开始,长 12
const result = new TextDecoder().decode(
  new Uint8Array(memory.buffer, 0, bytes.length));  // ⑤ 原地读回,解码
console.log(result); // "HELLO, WASM!"

读懂这一段的钥匙:传的不是数据,是「内存地址 + 长度」。 toUpperCase(0, 12) 两个参数分别是偏移和字节数——Wasm 拿着这两个数, 自己去线性内存里找数据、改数据。JS 和 Wasm 始终共享同一块内存,没有复制。15

3.6 安全:沙箱挡住了什么,没挡住什么

书里对安全的态度是「沙箱很强,但有三类残余风险」,我们照原样接住:16

沙箱默认挡住的:模块被关在线性内存里,没有操作系统的直接入口——文件 I/O、 网络请求、DOM 操作都做不了,除非 JS 显式给。内存相关攻击(越界、释放后重用) 也被校验阶段的边界检查挡住。17

没挡住的三种:

  • 侧信道攻击:比如利用共享 CPU 缓存做计时攻击(Spectre 类);
  • 恶意负载藏进二进制:Wasm 字节码紧凑,静态分析难,编译后的模块里可能藏着坏东西;
  • 供应链:来路不明的第三方 .wasm 可能带后门。18

对应的缓解(书里的清单):HTTPS + SubtleCrypto 验签名后再实例化; import 收紧(限制内存增长上限、运行时校验函数签名);必要时用 Binaryen 反汇编细看。19

内存封顶是最好用的一个旋钮,一行代码:

const memory = new WebAssembly.Memory({ initial: 1, maximum: 2 });  // 最多 128KB

把带 maximum 的 memory 作为 import 交出去,模块就永远长不过 128KB。20

3.7 主线例子:2×2 矩阵乘法(原书的例子,真数字)

这一章的原书例子是全章机制的总演习,数字全是书里的,可以直接复算。 输入:两个 2×2 整数矩阵(按行列排成的数表),a = [[1,2],[3,4]],b = [[5,6],[7,8]]。21

第 1 步:分配与接线。 new WebAssembly.Memory({ initial: 1 }) 开出 64KB; 实例化 matrix.wasm 时把这块内存作为 env.memory import 进去——模块和 JS 从此共用这条字节带。

第 2 步:摆数据。 用 Int32Array 视图把内存切成 4 字节一格:

矩阵 内存偏移(字节) 视图写法 初值
a 0 new Int32Array(buffer, 0, 4) [1, 2, 3, 4]
b 16 new Int32Array(buffer, 16, 4) [5, 6, 7, 8]
result 32 new Int32Array(buffer, 32, 4) 未初始化

三个 4 元素的 Int32 数组各 16 字节,首尾相接地摆在字节带的开头。22

第 3 步:调用。 instance.exports.multiply(0, 16, 32, 2)——四个参数是 a 的偏移、b 的偏移、result 的偏移、矩阵阶数 n=2。23

第 4 步:模块内部(照 C 源码走)。 三层循环,外两层扫 result 的每个格子, 内层做点积:result[i*n+j] += a[i*n+k] * b[k*n+j]。24 拿 (0,0) 格算一遍:0 先清零,然后 k=0:1×5=5;k=1:2×7=14;累加得 19。 (0,1) 格:1×6+2×8=22;(1,0):3×5+4×7=43;(1,1):3×6+4×8=50。

第 5 步:读回。 console.log(result) 打出 [19, 22, 43, 50]—— 正是 a×b 的真值,而且一步都没离开过那块共享内存。25

这一个例子把本章每个承重机制都占了一步:fetch+streaming(第 1 步)、 import 接线(第 1 步)、类型化数组视图(第 2 步)、地址+长度传参(第 3 步)、 线性内存原地读写(第 4、5 步)。

4. 作者的判断与证据

  • 「流式编译让大模块近乎即时启动」:机制描述与各家引擎的实现一致,属于可靠的工程事实;
  • 「沙箱能挡 use-after-free、整数溢出」:这句话要打个折扣——Wasm 的边界检查挡的是 越界读写,而「释放后重用」在模块自己管理的堆(比如 malloc 出来的内存)里照样可能发生, 沙箱管不到堆内逻辑。书里这句说得满了;
  • 「2×2 矩阵乘法」例子的数字:可复算,我们复算过,正确。26

判断(我们的,不是书里的): 这一章把「怎么跑起来」讲得很全,但把一个日常痛点说轻了—— memory.grow() 之后,JS 侧所有 Uint8Array 视图会当场失效(buffer 被引擎整个换掉), 必须重开视图。原书在 grow 的例子里完全没提这个坑,而它是在生产里天天咬人的。 如果错,会错在: 如果引擎实现对 buffer 做了透明迁移(目前 Chrome/Firefox 都会换 buffer), 这个提醒就是多余的;但以 2026 年的主流实现为准,它成立。

5. 边界与局限

  • 没有覆盖 Worker 里的 Wasm:线程化留到第 5 章;
  • 错误处理只到「兜底」为止:retry、超时、加载进度这些工程细节没有展开;
  • 共享内存多线程(SharedArrayBuffer)只在安全一节提了一句,它的跨源隔离政策(CORS/COOP/COEP)全书都没细讲——那是真要在生产用多线程时绕不开的一关(补充,不在书里);
  • 「模块不可信」的验证:验签名的具体流程只有一句话,没有完整代码。

6. 可带走的

  1. 生命周期四站:fetch → 编译 → 实例化 → 执行;编译出「模块」,实例化出「实例」,一个模块可实例化多次。
  2. 网络加载用 instantiateStreaming(边下边编译),离线/兜底用 instantiate(等下完)。
  3. import 对象是模块的「对外接口清单」:模块声明要什么,JS 在实例化时给什么;签名对不上立刻 TypeError。
  4. 线性内存按 64KB 页增长;grow() 返回的是旧页数。
  5. 跨边界传复合数据 = 编码进内存 + 传「偏移 + 长度」;字符串用 TextEncoder/TextDecoder。
  6. 类型化数组视图是「看法」不是「副本」:Uint8Array 看字节,Int32Array 看 32 位整数。
  7. 安全三旋钮:HTTPS + 验签、maximum 封顶内存、import 最小化。
  8. 恶意 .wasm 可能躲得过静态分析,第三方模块要追来源。
  9. 矩阵乘法的套路可推广:所有「数组进、数组出」的计算都是「摆数据 → 传地址 → 读回」三步。
  10. Wasm 挂了要有 JS 兜底——这不是多余,是发布前的必要检查项。

7. 原文地图

主题 原书章 原文位置
生命周期四站与流式编译 Executing WebAssembly in the Browser text/14-fm-ultimate-webassembly-for-high-performance-apps-b.txt:67(搜「fetching, compilation, and instantiation」) · text/14-fm-ultimate-webassembly-for-high-performance-apps-b.txt:79(搜「streaming compilation」)
instantiate 四步链 同上 text/14-fm-ultimate-webassembly-for-high-performance-apps-b.txt:133(搜「fetch('math.wasm')」)
instantiateStreaming 同上 text/14-fm-ultimate-webassembly-for-high-performance-apps-b.txt:235(搜「instantiateStreaming(fetch('math.wasm')」)
CDN 与 Webpack 加载 同上 text/14-fm-ultimate-webassembly-for-high-performance-apps-b.txt:309(搜「cdn.example.com」) · text/14-fm-ultimate-webassembly-for-high-performance-apps-b.txt:361(搜「asyncWebAssembly」)
异步错误处理与兜底 同上 text/14-fm-ultimate-webassembly-for-high-performance-apps-b.txt:453(搜「network interruptions」) · text/14-fm-ultimate-webassembly-for-high-performance-apps-b.txt:497(搜「initWasmWithFallback」)
内存分页与 grow 同上 text/14-fm-ultimate-webassembly-for-high-performance-apps-b.txt:575(搜「64KB pages」) · text/14-fm-ultimate-webassembly-for-high-performance-apps-b.txt:587(搜「Memory({ initial: 1, maximum: 10 })」)
字符串处理走查 同上 text/14-fm-ultimate-webassembly-for-high-performance-apps-b.txt:625(搜「TextEncoder」)
沙箱能力与残余风险 同上 text/14-fm-ultimate-webassembly-for-high-performance-apps-b.txt:751(搜「confining code to a linear memory space」) · text/14-fm-ultimate-webassembly-for-high-performance-apps-b.txt:755(搜「side-channel」)
内存封顶例子 同上 text/14-fm-ultimate-webassembly-for-high-performance-apps-b.txt:771(搜「maximum: 2」)
矩阵乘法 C 源码与 JS 同上 text/14-fm-ultimate-webassembly-for-high-performance-apps-b.txt:801(搜「void multiply」) · text/14-fm-ultimate-webassembly-for-high-performance-apps-b.txt:883(搜「matrixMultiply」) · text/14-fm-ultimate-webassembly-for-high-performance-apps-b.txt:899(搜「19, 22, 43, 50」)

  1. 出处:「Executing WebAssembly in the Browser」第 13–29 段(text/14-fm-ultimate-webassembly-for-high-performance-apps-b.txt:15,搜「mechanics of loading, compiling, and executing」)。 ↩

  2. 出处:「Executing WebAssembly in the Browser」第 93–107 段(text/14-fm-ultimate-webassembly-for-high-performance-apps-b.txt:93,搜「Fetching:」)。四站是原书 The WebAssembly Lifecycle 一节的列表。 ↩

  3. 出处:「Executing WebAssembly in the Browser」第 79 段(text/14-fm-ultimate-webassembly-for-high-performance-apps-b.txt:79,搜「as the bytes arrive」)。 ↩

  4. 出处:「Executing WebAssembly in the Browser」第 119–127 段(text/14-fm-ultimate-webassembly-for-high-performance-apps-b.txt:121,搜「WebAssembly.instantiate()」)。 ↩

  5. 出处:「Executing WebAssembly in the Browser」第 173–191 段(text/14-fm-ultimate-webassembly-for-high-performance-apps-b.txt:181,搜「import object」)。原文:「The second argument provides an import object」。 ↩

  6. 出处:「Executing WebAssembly in the Browser」第 247–259 段(text/14-fm-ultimate-webassembly-for-high-performance-apps-b.txt:255,搜「streams the Wasm file」)。 ↩

  7. 出处:「Executing WebAssembly in the Browser」第 453–461 段(text/14-fm-ultimate-webassembly-for-high-performance-apps-b.txt:457,搜「fallback to synchronous alternatives」)。 ↩

  8. 出处:「Executing WebAssembly in the Browser」第 477–493 段(text/14-fm-ultimate-webassembly-for-high-performance-apps-b.txt:479,搜「TypeErrors」)。 ↩

  9. 出处:「Executing WebAssembly in the Browser」第 497–513 段(text/14-fm-ultimate-webassembly-for-high-performance-apps-b.txt:497,搜「initWasmWithFallback」)。 ↩

  10. 出处:「Executing WebAssembly in the Browser」第 357–367 段(text/14-fm-ultimate-webassembly-for-high-performance-apps-b.txt:361,搜「asyncWebAssembly」)。 ↩

  11. 出处:「Executing WebAssembly in the Browser」第 571–583 段(text/14-fm-ultimate-webassembly-for-high-performance-apps-b.txt:575,搜「64KB pages」)。 ↩

  12. 出处:「Executing WebAssembly in the Browser」第 587–611 段(text/14-fm-ultimate-webassembly-for-high-performance-apps-b.txt:593,搜「initial: 1, maximum: 10」)。「返回旧页数」见 text/14-fm-ultimate-webassembly-for-high-performance-apps-b.txt:611(搜「previous page count」)。 ↩

  13. 出处:「Executing WebAssembly in the Browser」第 613–721 段(text/14-fm-ultimate-webassembly-for-high-performance-apps-b.txt:691,搜「view into the Wasm memory」)。 ↩

  14. 出处:「Executing WebAssembly in the Browser」第 617–721 段(text/14-fm-ultimate-webassembly-for-high-performance-apps-b.txt:621,搜「processString」)。 ↩

  15. 出处:「Executing WebAssembly in the Browser」第 697–705 段(text/14-fm-ultimate-webassembly-for-high-performance-apps-b.txt:701,搜「toUpperCase」)。原文:「The function modifies the memory in-place.」 ↩

  16. 出处:「Executing WebAssembly in the Browser」第 747–767 段(text/14-fm-ultimate-webassembly-for-high-performance-apps-b.txt:749,搜「risks remain」)。 ↩

  17. 出处:「Executing WebAssembly in the Browser」第 751 段(text/14-fm-ultimate-webassembly-for-high-performance-apps-b.txt:751,搜「without direct access to the underlying operating system」)。 ↩

  18. 出处:「Executing WebAssembly in the Browser」第 755–757 段(text/14-fm-ultimate-webassembly-for-high-performance-apps-b.txt:755,搜「timing attacks」)与 text/14-fm-ultimate-webassembly-for-high-performance-apps-b.txt:757(搜「Supply chain threats」)。 ↩

  19. 出处:「Executing WebAssembly in the Browser」第 759–765 段(text/14-fm-ultimate-webassembly-for-high-performance-apps-b.txt:759,搜「SubtleCrypto」)。 ↩

  20. 出处:「Executing WebAssembly in the Browser」第 769–793 段(text/14-fm-ultimate-webassembly-for-high-performance-apps-b.txt:771,搜「maximum: 2」)。 ↩

  21. 出处:「Executing WebAssembly in the Browser」第 795–815 段(text/14-fm-ultimate-webassembly-for-high-performance-apps-b.txt:801,搜「void multiply」)。 ↩

  22. 出处:「Executing WebAssembly in the Browser」第 883–943 段(text/14-fm-ultimate-webassembly-for-high-performance-apps-b.txt:891,搜「Int32Array(memory.buffer, 0, n * n)」)。 ↩

  23. 出处:「Executing WebAssembly in the Browser」第 945–967 段(text/14-fm-ultimate-webassembly-for-high-performance-apps-b.txt:945,搜「instance.exports.multiply」)。 ↩

  24. 出处:「Executing WebAssembly in the Browser」第 801–813 段(text/14-fm-ultimate-webassembly-for-high-performance-apps-b.txt:811,搜「result[i * n + j]」)。 ↩

  25. 出处:「Executing WebAssembly in the Browser」第 969–975 段(text/14-fm-ultimate-webassembly-for-high-performance-apps-b.txt:971,搜「19, 22, 43, 50」)。 ↩

  26. 出处:「Executing WebAssembly in the Browser」第 899 段(text/14-fm-ultimate-webassembly-for-high-performance-apps-b.txt:899,搜「19, 22, 43, 50」)。矩阵乘积经我们按定义复算,与书一致。 ↩