Rust + WebAssembly 全链路解析

第1章 为什么 Rust 需要 WebAssembly

作者 杨艺韬 · 15,433 字 · 发布于 · 更新于

"WebAssembly is the first solution for low-level code on the Web that delivers on all of the above design goals."("above" 指论文前文列出的 Safe、Fast、Portable、Compact 四个目标)— Haas et al.,《Bringing the Web up to Speed with WebAssembly》(PLDI 2017)

1.1 JavaScript 的性能天花板

要理解 WebAssembly 为什么出现,必须先理解 JavaScript 作为 Web 唯一编程语言所面临的根本性限制。这不是一个"JavaScript 不够快"的简单判断,而是一个关于语言设计、JIT 编译器架构和硬件执行的系统性问题。

动态类型的运行时代价

JavaScript 是动态类型语言:变量在运行时可以持有任何类型的值,引擎在每次操作时必须检查类型。V8 的处理方式是用"隐藏类"(Hidden Classes)和"内联缓存"(Inline Caches)做投机优化。如果函数 add(a, b) 的参数前 1000 次都是整数,TurboFan 编译器会假设第 1001 次也是整数,生成直接做整数加法的机器码。一旦假设失败——比如传入了一个字符串——整个优化代码块作废(deoptimization),回退到解释执行(Ignition),后续需要重新收集类型反馈才能再次优化。

flowchart TD
    A[JS 函数被频繁调用] --> B[Ignition 收集类型反馈]
    B --> C[投机编译为机器码]
    C --> D{类型稳定?}
    D -->|是| E[接近原生性能运行]
    D -->|否| F[去优化 deopt]
    F --> G[回退到 Ignition 解释执行]
    G --> H[重新收集类型反馈]
    H --> B
    style F fill:#dc2626,color:#fff
    style E fill:#10b981,color:#333

这种"乐观编译 + 悲观兜底"的策略对手写 JavaScript 效果很好——人类程序员很少在一个变量里交替存储整数和字符串。但对编译生成的代码并不对路。Emscripten 把 C++ 编译成 JavaScript 时,生成的代码模式极度统一(全是 HEAP32[x >> 2] | 0),这反而让投机优化失去了意义——类型永远"稳定",但每次内存访问都要经过 >> 2 把字节地址换算成数组下标、再用 | 0 强制成整数。

asm.js:第一个认真的尝试

2013 年,Mozilla 公布了 asm.js 规范(David Herman、Luke Wagner、Alon Zakai 执笔),配合 Alon Zakai 自 2010 年起开发的 Emscripten 编译器。asm.js 是 JavaScript 的一个严格子集,设计目标有两个:让引擎能识别出它、跳过常规 JIT 的类型推断与投机,直接提前(AOT)编译为机器码,同时保持合法 JavaScript 语法以在不支持 asm.js 的引擎上也能运行(只是慢一些)。

asm.js 的核心约定:

// asm.js 模块声明
function asmModule(stdlib, foreign, heap) {
  "use asm";

  var HEAP32 = new stdlib.Int32Array(heap);
  var abs = stdlib.Math.abs;

  function add(x, y) {
    x = x | 0;   // 强制声明 x 为 int32
    y = y | 0;   // 强制声明 y 为 int32
    return (x + y) | 0;  // 返回值也是 int32
  }

  function process(ptr, len) {
    ptr = ptr | 0;
    len = len | 0;
    var sum = 0;
    var i = 0;
    for (; (i | 0) < (len | 0); i = (i + 1) | 0) {
      sum = (sum + HEAP32[(ptr + (i << 2)) >> 2] | 0) | 0;
    }
    return sum | 0;
  }

  return { add: add, process: process };
}

asm.js 的 | 0 注解让 V8/SpiderMonkey 识别出这些操作是整数运算,跳过类型检查直接生成机器码。2013 年 3 月,Mozilla 与 Epic Games 宣布用 Emscripten 把 Unreal Engine 3 编译成 asm.js、不装插件就在 Firefox 里跑起来(Mozilla 博客)——这在当时是突破性的。

但 asm.js 的根本缺陷无法修补:

第一,它仍然是 JavaScript 文本。asm.js 模块即使做了最极致的压缩,体积仍然远大于等价的机器码——每条运算都要写成 (x + y) | 0 这样的源码文本。大型 C++ 项目编译到 asm.js 后体积可观,而解析这么大一坨 JavaScript 文本需要可观的时间——这是冷启动延迟的主要来源。

第二,类型系统是后验的。asm.js 的类型注解借用的是 JavaScript 的强制转换写法,引擎要先验证整个模块确实符合 asm.js 的类型规则。如果验证失败(比如某处 | 0 的操作数不是整数),引擎必须回退到普通 JavaScript 语义。这导致 asm.js 的性能不是"保证接近原生",而是"在引擎正确识别的路径上接近原生"。

第三,数值类型受制于 JavaScript。asm.js 没有 64 位整数——JavaScript 的 Number 装不下 int64,要加这种类型得先扩展 JavaScript 本身(Haas 等人的论文正是拿 int64 举例);编译器只能用两个 32 位值拼出 64 位运算。float32 要靠 Math.fround 标注,8/16 位整数只能作为堆视图(HEAP8、HEAP16)存取,运算仍按 32 位进行。依赖 64 位整数的哈希、密码学、大数代码因此要多付一笔本可避免的运算开销。

NaCl:另一个失败的尝试

在 asm.js 之前,Google 在 2011 年把 Native Client(NaCl)带进了 Chrome——把原生机器码沙箱化后直接在浏览器中执行。NaCl 的性能接近原生(WebAssembly 的设计论文也承认它是第一个"runs at near native speed"的 Web 机器码沙箱),但 Google 在 2017 年宣布弃用它的可移植版 PNaCl,转向 WebAssembly。它的结构性短板有三:一是 NaCl 分发的是特定架构的机器码,x86-32、x86-64、ARM 各要编译一份,违背了 Web 的"一次编写到处运行"原则(后来的 PNaCl 改用 LLVM bitcode 补救,但又把编译开销挪到了客户端);二是沙箱化靠对机器码做静态验证,要求代码生成器遵守指令对齐、访存与跳转前加掩码等特定模式,每种架构都要单独设计一套;三是它只在 Chrome 里有(论文原话是"NaCl and PNaCl are exclusively in Chrome"),非可移植的 NaCl 模块还只能随 Chrome Web Store 应用分发,无法成为跨浏览器的方案。

graph TD
    subgraph "NaCl(已废弃)"
        NA1[C/C++ 源码] --> NA2[x86 机器码]
        NA2 --> NA3[沙箱验证]
        NA3 --> NA4[直接执行]
        NA2 -.->|需按架构各编一份| NA5[x86-64 / ARM]
    end
    subgraph "asm.js(已淘汰)"
        AM1[C/C++ 源码] --> AM2[JavaScript 文本]
        AM2 --> AM3[解析 + 类型推断]
        AM3 --> AM4[投机编译]
        AM4 -.->|可能| AM5[去优化]
    end
    subgraph "WebAssembly"
        WA1[任意语言源码] --> WA2[".wasm 二进制"]
        WA2 --> WA3[验证 → 直接编译]
        WA3 --> WA4[稳定执行]
        WA2 -.->|可移植| WA5[x86 / ARM / RISC-V]
    end
    style NA4 fill:#dc2626,color:#fff
    style AM4 fill:#f59e0b,color:#333
    style WA4 fill:#10b981,color:#333

NaCl 和 asm.js 的失败为 WebAssembly 的设计提供了清晰的方向:需要一种二进制格式(不是文本)、可移植(不是 x86 专用)、类型安全(不是后验验证)、紧凑(不是臃肿的 JavaScript 文本)的编译目标。

WebAssembly 的直接回应

2015 年,W3C 的 WebAssembly Community Group 成立(4 月提议建组,6 月 17 日四家浏览器厂商同日公开宣布 WebAssembly),设计目标直接瞄准了 NaCl 和 asm.js 的痛点。WASM 是强类型的——类型信息在二进制中编码,虚拟机不需要做类型推断;WASM 是二进制格式的——解码只是按段读取长度前缀和 LEB128 整数,比解析等价的 JavaScript 文本快一个数量级(设计仓库 FAQ 引用的早期实验是 20 倍以上);WASM 有显式内存模型——线性内存访问越界在运行时直接 trap,不需要运行时类型标记;WASM 是可移植的——同一个 .wasm 二进制在 x86、ARM、RISC-V 上都能执行。

可核实的数据来自 WebAssembly 的设计论文《Bringing the Web up to Speed with WebAssembly》(PLDI 2017):在 PolyBenchC 基准集上,有 7 个用例落在原生代码的 10% 以内,几乎全部在 2 倍以内。同一套基准跑在 asm.js 上要慢一截——论文给出的数字是 WASM 平均比 asm.js 快 33.7%,在 SpiderMonkey 上验证耗时不到 asm.js 的 3%(论文 PDF)。

PolyBenchC 都是小型数值内核。换成更大、更贴近真实程序的 SPEC CPU 基准,差距就明显了:Jangda 等人的《Not So Fast: Analyzing the Performance of WebAssembly vs. Native Code》(USENIX ATC 2019)测得 WASM 平均比原生慢 45%(Firefox)到 55%(Chrome),最坏达 2.08 倍和 2.5 倍,原因包括寄存器压力更大、额外的栈溢出与间接调用检查带来更多分支等(arXiv:1901.09056)。所以"接近原生"要看负载:小内核可以很接近,大程序通常仍有可观差距。

graph LR
    subgraph "JavaScript / asm.js"
        A[源码文本] --> B[解析 + 类型推断]
        B --> C[投机编译]
        C --> D{类型稳定?}
        D -->|是| E[优化后的机器码]
        D -->|否| F[去优化 → 回退解释执行]
    end
    subgraph "WebAssembly"
        G[二进制] --> H[解码 → 验证 → 直接编译]
        H --> I[稳定执行,无去优化]
    end
    style E fill:#f59e0b,color:#333
    style F fill:#dc2626,color:#fff
    style I fill:#10b981,color:#333

WASM 的性能模型是可预测的:类型在编译前就已确定,不会因为类型 profile 变化而触发去优化、出现性能悬崖。这对生产系统至关重要——没有人愿意在发布后才发现某个代码路径因为类型不稳定而性能暴跌。

1.2 WebAssembly 的设计哲学

理解 WASM 的设计选择,需要回到它要解决的问题。WebAssembly 核心规范(1.0,2019 年 12 月 5 日成为 W3C 推荐标准)在引言的"Design Goals"一节列出了一组设计目标:语义上要快(near-native performance)、安全、定义明确、与硬件/语言/平台无关;表示上要紧凑、模块化、可流式与并行解码编译、可移植。下面把它们归并成三条原则——从指令集选择到内存模型到安全沙箱,都可以用这三条原则解释。

原则一:可预测的接近原生的性能

WASM 的指令集贴近硬件——大量指令能直接映射到一条机器指令。i32.add 在 x86-64 上直接编译为 add,f64.mul 直接编译为 mulsd,i32.load 直接编译为 mov 加偏移量寻址。没有隐式装箱、没有类型标记、没有运行时类型检查。

这让 WASM 的性能天花板可以逼近原生代码,差距来自虚拟化的开销,以及 WASM 引擎代码生成质量与原生编译器的差距(上文 Jangda 等人找到的原因两类都有)。虚拟化这层开销主要来自三个地方:

  1. 验证开销:模块加载时需要验证类型安全、控制流合法性(内存访问是否越界不在验证期检查,见第 3 点)。验证是 O(n) 的,n 是模块大小,只在加载时执行一次。
  2. 编译开销:编译器把 WASM 指令翻译为机器码。V8 采用两级策略:Liftoff 做一遍线性扫描快速生成未优化机器码让模块尽早开跑,TurboFan 在后台生成更优化的版本替换掉它。Wasmtime 同样有两个后端——默认的 Cranelift 追求代码质量,Winch 是牺牲代码质量换编译延迟的基线编译器(见 crates/wasmtime/src/config.rs 的 Strategy 枚举)。
  3. 边界检查:每次线性内存访问需要检查地址是否在 [0, memory.size) 范围内。在 64 位平台上,V8、SpiderMonkey、Wasmtime 都用"保护页 + 信号处理器"的虚拟内存技术:为 32 位线性内存预留一大段虚拟地址空间,有效部分之外全部标为不可访问,越界访问直接触发 SIGSEGV,由信号处理器转为 WASM trap——绝大多数显式边界检查因此可以省掉(Haas 等人的论文 §7.1 描述了这一做法)。32 位平台和 64 位地址的 memory64 用不了这招,仍需显式检查。
graph LR
    A[原生性能] --> B["- 验证开销(一次性)"]
    A --> C["- 编译开销(一次性 + 后台)"]
    A --> D["- 边界检查(64 位平台上多数被保护页消掉)"]
    A --> F["- 代码生成质量差距(寄存器分配、额外检查等)"]
    A --> E["= WASM 的实际性能:可预测,负载不同差距不同"]
    style E fill:#10b981,color:#333

原则二:安全沙箱

WASM 代码运行在沙箱中:它不能直接访问宿主内存,只能通过导入/导出的函数和线性内存与外界交互。没有指向宿主内存的指针、没有系统调用、没有文件 I/O——所有对外操作都必须经过宿主的显式授权。

graph TB
    subgraph "WASM 沙箱边界"
        WA[WASM 模块] --> WB[线性内存]
        WA --> WC[导出函数]
        WA --> WD[表 Table]
        WA --> WE[全局变量]
    end
    subgraph "宿主(浏览器/运行时)"
        HA[JS 全局对象] -->|调用导出函数| WC
        WC -->|返回值| HA
        HB[ArrayBuffer] -.->|共享线性内存| WB
        HC[系统 API] -.->|需要 WASI 授权| WA
    end
    WA -.禁止直接访问.- HF[DOM / 网络 / 文件系统]
    style WA fill:#6366f1,color:#fff
    style HA fill:#22d3ee,color:#333
    style HF fill:#dc2626,color:#fff

这个沙箱模型和浏览器安全模型天然契合:一个 WASM 模块不能突破它的线性内存边界,就像一个跨源 iframe 不能读取父页面的 DOM。宿主不提供文件相关的导入(在服务端即不授予 WASI 能力),WASM 模块就根本碰不到文件系统。

沙箱的安全性不仅是功能性的,还是可验证的——WASM 的验证器在模块加载时就能保证执行安全。验证通过 = 执行不会出现类型错误、不会跳到非法位置;越出线性内存的访问会在运行时 trap,而不是读写到沙箱之外。这意味着你可以安全地执行不受信任的 WASM 代码,不需要像容器那样依赖操作系统级别的隔离。

原则三:二进制级可移植性

WASM 不依赖任何特定的硬件架构或操作系统。同一个 .wasm 文件可以在 x86-64、ARM、RISC-V 上运行,可以在 Chrome、Firefox、Safari、Node.js、Wasmtime、Wasmer 上运行。这种可移植性是"二进制级"的——不需要在每个平台上重新编译,同一个二进制文件到处执行。

二进制级可移植性和 Java 的"一次编写到处运行"看起来相似,但设计哲学不同。Java 的可移植性依赖 JVM——一个复杂的、包含 GC 和 JIT 的虚拟机;WASM 的可移植性依赖一个极简的、栈式的虚拟指令集和线性内存模型。WASM 核心规范不含对象模型、类加载这类机制,远比 JVM 规范精简。简洁性是 WASM 能被所有主流浏览器同时实现的前提。

1.3 为什么 Rust 是最佳 WASM 语言

在所有能编译到 WASM 的语言中——C、C++、Go、C#、AssemblyScript、Kotlin——Rust 为什么脱颖而出?这不是偏好问题,而是语言运行时模型和 WASM 设计约束的系统性匹配。

零成本抽象 → 小体积 + 快执行

Rust 的核心设计信条是"零成本抽象":你不需要为不使用的功能付出运行时代价。这对 WASM 来说至关重要,因为 WASM 模块的体积直接影响冷启动延迟和带宽消耗。

没有 GC 运行时。Go 编译到 WASM 需要包含整个 GC 运行时——标记-清扫器、goroutine 调度器、栈增长逻辑。光是运行时就要占掉 MB 量级的 .wasm 体积。C#(Blazor WebAssembly)更夸张,运行时加框架还要更大。Rust 没有垃圾回收器,所有权系统在编译期完成内存管理,编译出的 WASM 代码只包含你实际使用的逻辑。

没有隐式异常处理。C++ 编译到 WASM 时,异常处理会引入大量胶水代码。WASM 的异常处理提案(Wasm EH)在浏览器里默认可用得相当晚——Chrome 95(2021-10)、Safari 15.2(2021-12)、Firefox 100(2022-05)才陆续默认开启(web-features 数据),且即使启用了,C++ 的异常表和 landing pad 也会显著增加二进制体积。Rust 的 Result 类型是普通值;panic 在 wasm32-unknown-unknown 上默认走 abort 策略(📄 rust compiler/rustc_target/src/spec/base/wasm.rs 的 panic_strategy: PanicStrategy::Abort),最终执行 unreachable 指令触发 trap,JS 侧捕获到的是 WebAssembly.RuntimeError(想在控制台看到 panic 信息,可以装 console_error_panic_hook),不需要异常表。

单态化消除泛型开销。Rust 的泛型在编译时单态化(monomorphization),生成具体类型的特化代码。WASM 虚拟机看到的是特化后的直代码,没有虚表查找、没有动态分发(除非你显式使用 dyn Trait)。这与 C++ 的模板实例化类似,但 Rust 的单态化更加可控——PhantomData 和零大小类型(ZST)在编译后被完全消除。

把同一个哈希计算功能编译到 WASM,各语言产物的体积量级差异来自"必须捎带什么运行时":

语言 体积量级 必须捎带的运行时
Rust 几十 KB 无(仅 dlmalloc 分配器,可替换)
C(Emscripten -Oz) 几十 KB 无(但链接了 libc 的用到部分)
Go MB 量级 GC + goroutine 调度器
C#(Blazor) MB 量级 .NET 运行时
AssemblyScript 几十 KB 编进模块的轻量 GC 运行时(默认 TLSF 分配器 + 增量 GC,可换 minimal/stub)

Rust 和 C 在同一个量级,但 Rust 提供了内存安全保证——这是 C 无法给出的。具体的体积构成和压缩手段留到第 9 章拆。

所有权系统 → 线性内存的安全使用

WASM 的内存模型是线性的:一段连续的字节数组,从地址 0 开始编址,通过 i32.load/i32.store 指令访问。没有虚拟内存、没有页表、没有内存保护——模块内部可以随意读写自己的线性内存。

这和 C 语言的 malloc/free 模型一样危险——悬垂指针、双重释放、缓冲区溢出,全部可能发生。而且线性内存内部缺少原生平台上那些缓解措施:地址 0 是合法地址(空指针解引用不会崩溃),没有只读页、没有栈与堆之间的保护页、没有地址随机化(Lehmann 等人的《Everything Old is New Again: Binary Security of WebAssembly》,USENIX Security 2020 对此有系统分析)。只有越出整个线性内存的访问才会 trap;在线性内存范围内的越界写(比如写到了另一个数据结构的区域)不会触发 trap——WASM 不做运行时内存布局验证。

Rust 的所有权系统在编译期就排除了这些问题:

// Rust: 编译期保证安全
fn process(data: &[u8]) -> u32 {
    let val = u32::from_le_bytes(data[0..4].try_into().unwrap());
    val * 2
}

编译到 WASM 后,Rust 的边界检查变成了几条额外的比较加条件跳转指令——代价是可以预测的常数级开销,换来的是内存安全保证。如果用 unsafe 跳过边界检查:

fn process_unchecked(data: &[u8]) -> u32 {
    // 跳过边界检查:data 不足 4 字节时是未定义行为,
    // WASM 只在地址超出整个线性内存时才 trap
    unsafe {
        let val = u32::from_le_bytes(
            data.get_unchecked(0..4).try_into().unwrap()
        );
        val * 2
    }
}

即使在 unsafe 块中,WASM 的线性内存边界检查仍然会在访问超出 memory.size 的地址时触发 trap。但在 memory.size 范围内的越界写(写到相邻数据结构的区域),WASM 无法检测——这正是 Rust 所有权系统要防止的情况。

graph TB
    subgraph "Rust 所有权检查(编译期)"
        R1[引用是否有效?]
        R2[借用是否冲突?]
        R3[生命周期是否满足?]
        R1 -->|通过| R4[编译成功]
        R2 -->|通过| R4
        R3 -->|通过| R4
        R1 -->|失败| R5[编译错误]
        R2 -->|失败| R5
        R3 -->|失败| R5
    end
    subgraph "WASM 运行时检查(加载时 + 执行时)"
        W1[类型是否一致?]
        W2[控制流是否结构化?]
        W3[内存访问是否越界?]
        W1 -->|通过| W4[验证成功]
        W2 -->|通过| W4
        W3 -->|运行时 trap| W5[WASM trap]
    end
    R4 --> W4
    R4 --> W3
    style R4 fill:#10b981,color:#333
    style W4 fill:#10b981,color:#333
    style R5 fill:#dc2626,color:#fff
    style W5 fill:#dc2626,color:#fff

关键洞察:Rust 的编译期检查和 WASM 的运行时检查是互补的,不是冗余的。safe Rust 防止 memory.size 范围内的逻辑越界(写到错误的数据结构),WASM 防止 memory.size 范围外的物理越界(访问未分配的内存)。两者组合,才能在不写 unsafe 的前提下实现完整的内存安全。

工具链生态成熟

Rust 的 WASM 工具链是各语言里最完整的之一。看几项具体功能的对照(其他语言一栏只列官方或主流方案,"无"指没有对应的一体化工具):

功能 Rust 工具 C/C++ 工具 Go 工具 C# 工具
语言↔JS 绑定生成 wasm-bindgen embind syscall/js JS interop
一键构建+测试+发布 wasm-pack 无 无 无
组件模型绑定 wit-bindgen Rust 生成器 wit-bindgen C/C++ 生成器 wit-bindgen go 生成器(旧的 TinyGo 生成器已不再维护) wit-bindgen C# 生成器
浏览器端测试 wasm-bindgen-test 无 无 无
npm 集成 wasm-pack 内置 手动 手动 Blazor SDK
多目标编译 wasm32-unknown-unknown / wasm32-wasip1 / wasm32-wasip2 Emscripten GOOS=js wasm target

wasm-pack 是 Rust 的一大优势——build、test、pack、publish 几个子命令覆盖了从编译、生成 JS 绑定、在浏览器/Node 里跑测试到发布 npm 包的全流程,一个 CLI 就够。wasm-bindgen-test 则让你在真实浏览器环境中运行 #[wasm_bindgen_test] 测试,验证 JS-WASM 互操作的正确性。

更重要的是,Rust 的 WASM 工具链和 Cargo 生态无缝集成。cargo build --target wasm32-unknown-unknown 可以编译任何不依赖系统 API 的 Rust crate 到 WASM。这意味着 Rust 生态中大量纯计算库(密码学、解析器、数据结构、算法)可以直接在浏览器中使用——sha2、serde_json、regex、comrak,大多只需要换一个编译目标(要随机数的 crate 得给 getrandom 选 JS 后端,ring 这类含 C/汇编代码的 crate 还需要能产出 wasm 的 C 编译器)。

1.4 编译目标的演进:从 NaCl 到 asm.js 到 WASM

WebAssembly 不是凭空出现的。它是浏览器端高性能计算需求长达十年的演进结果。理解这段历史,才能理解 WASM 的设计选择为什么是这样的。

NaCl → PNaCl → 废弃(2011-2017)

Google 在 2011 年把 Native Client(NaCl)带进 Chrome 14,最初只对 Chrome Web Store 应用开放,允许 C/C++ 编译为原生机器码在 Chrome 中运行(Chromium 博客)。最初的 x86-32 版沙箱基于 x86 段式内存和指令对齐验证——在机器码加载时做静态分析,确保没有跳转到沙箱外的指令。x86-64 和 ARM 没有可用的段机制,NaCl 为它们各自重新设计了基于地址掩码的沙箱(Sehr 等人,USENIX Security 2010)——能做到,但每种架构一套实现、一份机器码。

2013 年,Google 推出 Portable NaCl(PNaCl),用 LLVM bitcode 的一个稳定子集作为分发格式,在客户端编译为本地机器码。PNaCl 解决了指令集可移植性问题,但 Haas 等人的论文指出它在紧凑性上没有明显改进,仍暴露调用栈布局等编译器/平台细节,而且只在 Chrome 里可用。

2017 年 5 月 30 日,Google 在 Chromium 博客上宣布弃用 PNaCl、转向 WebAssembly:理由是 WebAssembly 已获得跨浏览器支持的势头,而 PNaCl 的使用量低到足以弃用;计划在 2018 年第一季度起,除 Chrome Apps 与扩展外移除 PNaCl 支持(原文)。

asm.js → 废弃(2013-2017)

asm.js 是 Mozilla 的方案,把 C/C++ 编译成 JavaScript 的严格子集。它在所有浏览器上都能运行(作为普通 JavaScript),但在 V8 和 SpiderMonkey 上能被识别并加速。asm.js 的问题前面已经分析过:体积大、类型后验、缺少 64 位整数等类型。

2017 年 WebAssembly MVP 在四大浏览器先后默认开启后,新的高性能代码都转向了 WASM。Emscripten 在 1.38.1(2018-05)把默认输出从 asm.js 换成了 WebAssembly(📄 Emscripten ChangeLog.md:"Breaking change: Emit WebAssembly by default")。

WebAssembly 的教训汇总

NaCl 和 asm.js 的失败为 WASM 的设计提供了三条明确教训:

教训 NaCl 的问题 asm.js 的问题 WASM 的解决方式
可移植性 分发特定架构的机器码 文本格式可移植但体积巨大 二进制格式 + 虚拟指令集
安全验证 原生机器码验证需按架构各做一套 类型规则后验,验证失败就退回普通 JS 类型编码在二进制中,验证器前置
体积 原生代码体积小但不通用 JavaScript 文本体积巨大 LEB128 变长编码 + 栈式指令集
timeline
    title 浏览器高性能计算的演进
    2011 : NaCl 进入 Chrome (原生机器码沙箱)
    2013 : asm.js 发布 (JS 子集) / PNaCl 发布 (LLVM bitcode)
    2015 : W3C WebAssembly CG 成立 / 6 月公开宣布 WebAssembly
    2017 : WASM MVP 在四大浏览器默认开启 / Google 宣布弃用 PNaCl
    2018 : Emscripten 默认输出 .wasm
    2019 : WASI 公布 / Core 1.0 成为 W3C 推荐标准(12 月)
    2021 : Component Model 提案仓库建立,在 CG 提案流程中列为 Phase 1(至今未变)
    2022 : Wasm 2.0 基本完成(SIMD、批量内存、多返回值、引用类型)
    2024 : WASI Preview 2 达到 Stable / WasmGC 集齐四大浏览器(Safari 18.2)
    2025 : Wasm 3.0 完成(GC、异常处理、多内存、64 位地址空间并入核心规范)/ wit-bindgen 多语言绑定成熟

1.5 Rust + WASM 的典型应用场景

理解了 Rust + WASM 的优势,接下来看它在真实场景中的表现。不是所有场景都适合 Rust + WASM——它在计算密集、低层次数据操作的场景中有结构性优势,在 DOM 操作和高频 JS API 调用的场景中有结构性劣势。

图像处理

用 Rust + WASM 替代 Canvas API 的逐像素操作是最常见的落地形态。第 19 章 §19.2 复盘了一个 4K 图像灰度转换的案例,结论却是反直觉的:同机实测 JS 约 35ms,WASM 标量版因为要经 wasm-bindgen 复制数据反而约 55ms,数据留在线性内存里计算约 44ms,SIMD 版约 39.5ms——换成 WASM 不等于变快,先测量再决定。

// Rust 处理 ImageData 的核心逻辑
#[wasm_bindgen]
pub fn gaussian_blur(data: &mut [u8], width: u32, height: u32, radius: u32) {
    // 直接操作像素数据,无需 JS 中转
    let kernel = generate_gaussian_kernel(radius);
    horizontal_pass(data, width, height, &kernel);
    vertical_pass(data, width, height, &kernel);
}

类似的案例包括 Figma(用 C++ 写的编辑器与 WebGL 渲染引擎整体编译到 WASM,Figma 博客,2017)和 AutoCAD Web(AutoCAD 的 C++ 代码库经 Emscripten 编译到 WASM)。

密码学

浏览器端密码学操作(哈希、签名验证、密钥派生)对性能和侧信道安全都有要求。Rust 编译到 WASM 后,整数运算不必经过 JS 的类型守卫;而且 Rust 的析构是确定性的,密钥材料可以在离开作用域时立即清零(zeroize crate 用 volatile 写实现,不会被编译器当作死存储优化掉)——这在 JS 中无法保证,因为引擎的 GC 可能在搬移对象时留下副本,程序无从控制。

// Cargo.toml:argon2 = { version = "0.5", default-features = false, features = ["alloc"] }
//             zeroize = "1"
// (argon2 的默认特性会拉入 getrandom,在 wasm32-unknown-unknown 上编译不过)
use argon2::Argon2;
use wasm_bindgen::prelude::*;
use zeroize::Zeroizing;

#[wasm_bindgen]
pub fn derive_key(password: &[u8], salt: &[u8]) -> Result<Vec<u8>, JsError> {
    // 中间缓冲区包在 Zeroizing 里:离开作用域(包括 ? 提前返回)时确定性清零
    let mut key = Zeroizing::new([0u8; 32]);
    Argon2::default()
        .hash_password_into(password, salt, key.as_mut())
        .map_err(|e| JsError::new(&e.to_string()))?;
    Ok(key.to_vec()) // 注意:返回给 JS 的这份拷贝不在 zeroize 的保护范围内
}

清零的边界要看清:Zeroizing 只管 Rust 自己持有的缓冲区。password 由 JS 胶水复制进线性内存,调用结束后由 Rust 侧释放但不清零;返回的 Vec<u8> 被复制成 JS 的 Uint8Array、交给 GC 管辖,线性内存里的原件由胶水调 __wbindgen_free 释放,同样不清零。真正要做到"密钥不出 WASM",应该让派生出的密钥只在 Rust 内部使用,只把签名、密文这类结果交给 JS。

解析器

将 Markdown 解析器、JSON 解析器、protobuf 解析器编译到 WASM,可以在浏览器端复用同一份经过充分测试的解析逻辑。至于 comrak(Rust 的 CommonMark 解析器)编译到 WASM 后能否快过 markdown-it 这类高度优化的 JS 实现,要连同跨边界传字符串的成本一起实测。serde_json 对比浏览器内置的 JSON.parse 更不一定占优——JSON.parse 是引擎内的 C++ 实现,还省掉了跨边界传字符串的成本,文档越大越可能反超。第 10 章会给出这类对比的测量方法。

插件系统

WASM 的沙箱模型天然适合做插件系统:宿主暴露一组受控 API,插件只能通过这些 API 与宿主交互,无法突破沙箱。Extism(通用 WASM 插件框架)和 Fermyon Spin(边缘计算框架)都基于这个模型。

与 Docker 容器相比,WASM 插件在三个维度上有结构性优势:

维度 Docker 容器 WASM 插件
冷启动 百毫秒量级(创建命名空间、挂载文件系统) 预编译产物缓存后,实例化在微秒到亚毫秒量级(首次验证 + 编译另计)
内存开销 至少一个完整进程及其运行库 线性内存按需增长,起步只有几页
安全边界 操作系统级隔离(namespace + cgroup) 指令级隔离(验证器保证)

这个对比在本专栏第 17 章还会展开——Web 框架的路由层可以嵌入 WASM 插件来执行用户自定义逻辑,而不需要为每个用户启动一个容器。

graph TB
    subgraph "Rust + WASM 擅长"
        G1[计算密集: 图像/音视频处理]
        G2[加密/压缩: SHA/argon2/zstd]
        G3[解析器: JSON/Markdown/protobuf]
        G4[插件系统: 沙箱化隔离]
        G5[数据转换: 格式转换/编码解码]
    end
    subgraph "Rust + WASM 不擅长"
        B1[DOM 操作: 需要经过 JS 中转]
        B2[高频 JS API 调用: 跨边界开销累积]
        B3[首屏关键路径: 冷启动延迟]
        B4[需要大量堆分配: GC 语言可能更合适]
    end
    style G1 fill:#10b981,color:#333
    style G2 fill:#10b981,color:#333
    style G3 fill:#10b981,color:#333
    style G4 fill:#10b981,color:#333
    style G5 fill:#10b981,color:#333
    style B1 fill:#dc2626,color:#fff
    style B2 fill:#dc2626,color:#fff
    style B3 fill:#dc2626,color:#fff
    style B4 fill:#dc2626,color:#fff

1.6 WASM 在技术栈中的定位

WASM 不是孤立的技术——它在浏览器、服务器、边缘的技术栈中各有位置。理解这些定位有助于判断"什么场景应该用 WASM、什么场景该用别的"。

1.6.1 浏览器端:WASM 与同代技术的关系

浏览器中能跑代码的技术不止 WASM——Web Workers、WebGL、WebGPU、Service Worker 都是同代选项。它们解决不同的问题,常常组合使用:

graph TD
  A["浏览器端计算需求"] --> B{"瓶颈在哪?"}
  B --> C["主线程阻塞"]
  B --> D["CPU 密集计算"]
  B --> E["GPU 并行计算"]
  B --> F["离线/缓存策略"]
  C --> C1["Web Workers<br/>+ JS 或 WASM<br/>把任务挪出主线程"]
  D --> D1["WASM<br/>计算密集 + 算法库丰富"]
  E --> E1["WebGPU<br/>大规模并行(图像/ML/物理)"]
  F --> F1["Service Worker<br/>JS only"]
  style C1 fill:#6366f1,color:#fff
  style D1 fill:#10b981,color:#333
  style E1 fill:#f59e0b,color:#333
  style F1 fill:#dc2626,color:#fff

实际项目常常组合多种:图像编辑器用 Service Worker 缓存 WASM 模块、用 Worker 跑 WASM 的滤镜计算、用 WebGPU 渲染最终结果。每种技术解决一个特定瓶颈,互不替代。

性能定位的全景对比(以"图像 4K 高斯模糊"这类逐像素滤波为例,耗时按从慢到快排序):

方案 相对耗时 开发难度 兼容性
纯 JS(Canvas 2D) 基准 低 全部浏览器
JS + Worker 与基准同量级(但不阻塞主线程) 中 全部浏览器
WASM(含 SIMD) 通常明显快于基准 中 全部现代浏览器(SIMD 需较新版本)
WebGL fragment shader 通常更快 高 几乎全部浏览器
WebGPU compute shader 通常最快 高 仅较新版本浏览器

具体倍数随图像尺寸、kernel 大小、设备而变,第 10 章会给出可复现的测量方法。WASM 的定位是中间层——通常比纯 JS 快,比 GPU 路线慢,但兼容性和开发难度都比 GPU 友好。

1.6.2 服务器端:WASM 与容器的对比

服务器端 WASM 经常被拿来和 Docker 容器对比——但二者解决的问题层级不同:

维度 Docker 容器 WASM 组件
隔离粒度 进程级(Linux namespace) 实例级(线性内存沙盒)
启动时间 百毫秒量级 亚毫秒到毫秒量级
内存开销 至少一个完整进程 线性内存 + 少量运行时状态
分发单元 OS 镜像(百 MB 到 GB 级) 单个 .wasm(KB 到 MB 级)
I/O 模型 完整文件系统 + 网络栈 WASI 受限子集
多语言支持 完整(任何 Linux 二进制) 编译到 WASI 的语言
生产成熟度 极高 中等(2024-2026 快速成熟)

WASM 不会取代 Docker——它们的关系类似 进程 vs 函数:Docker 适合长期运行的服务,WASM 适合短生命周期的边缘函数、插件、隔离工作负载。

1.6.3 与 JVM/CLR 的对比

WASM 经常被类比为"Web 上的 JVM"——但有几个本质差异:

维度 JVM WASM
设计目标 Java 字节码执行 通用编译目标(语言无关)
内存模型 自动 GC 线性内存(GC 类型在 Wasm 3.0 才并入,且可选用)
类型系统 OOP(class/interface) 极简(数值 + 函数)
启动时间 慢(类加载) 快(流式编译)
沙箱粒度 类加载器 模块边界

WASM 的设计哲学是"最小化 + 可扩展"——核心规范故意保持极简,复杂能力通过提案逐步加入(GC、Threads、SIMD;Component Model 则是叠在核心规范之上的另一层)。JVM 字节码则从一开始就内置了对象模型、GC 与类加载。

1.7 行业采用现状

理论价值之外,WASM 的真实采用情况是判断技术是否值得投入的最直接指标。下面是截至撰稿时的概况:

1.7.1 浏览器端的采用

浏览器端 WASM 集中在几类场景:图像/视频处理、游戏与 3D 渲染、加密与安全、数据可视化、文档处理、端侧 ML 推理。共同点是"计算密集 + 数据以字节数组形式存在 + 与 DOM 交互少"。

代表性应用:

  • 设计与图形:Figma(C++ → WASM 的渲染与文档引擎)、Adobe Photoshop Web(经 Emscripten 移植既有 C++ 代码库)
  • 3D/游戏与 CAD:Unity Web 构建、AutoCAD Web
  • ML 推理:TensorFlow.js 的 WASM 后端、ONNX Runtime Web、Transformers.js
  • 地图:Google Earth Web(原先基于 Chrome 独占的 NaCl,改编译到 WASM 后才在 2020 年登陆 Firefox、Edge、Opera)

加载 WASM 的站点在全网占比仍不到 1%:HTTP Archive 的 Web Almanac 2025 测得桌面站点 0.35%、移动站点 0.28%(约 4.3 万个站点),比 2021 年的约 0.04% 涨了一个量级,但近两年基本持平;在排名前 1000 的站点里,桌面端占比达到 2%(Web Almanac 2025 · WebAssembly)。WASM 至今仍是"重活场景的专用工具",不是通用的页面构件。

1.7.2 服务器端的采用

服务器端 WASM 的采用比浏览器更具行业集中度:

行业/平台 主要用例 工具栈
边缘 CDN 边缘函数 Cloudflare Workers、Fastly Compute
数据库/分析引擎 用户自定义函数(各家自研,尚无统一标准) 自建 WASM 宿主
服务网格 Envoy filter Proxy-Wasm(Istio 等)
区块链 智能合约 / 链上运行时 NEAR(合约)、Polkadot(运行时)
嵌入式 IoT 应用沙盒 Wasmtime、WAMR

其中 Proxy-Wasm 是最能说明问题的一个:Envoy 把"用户自定义流量处理逻辑"这件原本要靠重编译 C++ 或起 sidecar 才能做的事,换成了加载一个 .wasm 过滤器。这正是 WASM 在服务端最稳的定位——受控扩展点,而不是通用运行时。

1.7.3 工具链生态成熟度

WASM 工具链在 2024-2026 年快速成熟。关键里程碑:

  • 2024-01:WASI Preview 2(0.2.0)发布,组件模型作为其基础落地
  • 2024-10:wasm-bindgen 0.2.95 发布,Rust ↔ JS 互操作趋于稳定
  • 2025-01:wasm-bindgen 0.2.100 发布
  • 2026-04:wasm-bindgen 0.2.118、Wasmtime 44.0.0 发布——本专栏的基准版本;Wasmtime 44 内置的 WASI 0.3 接口是候选版 0.3.0-rc-2026-03-15(📄 Wasmtime crates/wasi/src/p3/wit/deps/cli.wit:1)
  • 2026-06-11:WASI 0.3.0 正式发布,组件模型原生 async 进入 WASI(本专栏对照的 Wasmtime 44 仍是上面那个 rc 实现)

工具链成熟度的实操指标:

工具 2024 状态 2026 状态
wasm-pack 稳定 稳定
wasm-bindgen 稳定 稳定 + 性能优化
Wasmtime 生产就绪 生产就绪 + 多语言
cargo-component 早期 可用于生产,但版本号仍在 0.x(撰稿时 crates.io 最新为 0.21.1)
浏览器调试 基础 借 DWARF 扩展可做源码级调试,仍不如 JS

1.7.4 学习曲线与生态成本

引入 WASM 的隐性成本不在技术——在团队:

  • Rust 学习曲线:以数月计才能达到能独立交付生产代码的水平(作者经验估计,有 C++ 或 Go 背景会快些)
  • WASM 工程化:还要额外花时间掌握 wasm-pack/wasm-bindgen 工作流
  • 运维新链路:构建产物多了 .wasm,CI 需要额外一条 Rust 工具链,构建时间明显变长
  • 调试技能:WASM 的调试体验仍落后于纯 JS,需要新工具和新习惯

这些成本对小团队往往很重——除非业务对性能的依赖明显。团队规模大一些,就可以安排少数 Rust 工程师专做"性能基础设施",把成本摊薄。本章 1.11 节给出的成本-收益判断框架,正是为了把这笔账算清楚。

1.8 如何阅读本专栏

本专栏把 Rust + WASM 的全链路拆成了 20 章——但不是所有读者都需要从第 1 章读到第 20 章。根据角色和目标,有三条不同的阅读路径。

1.8.1 三类读者画像

graph TD
  A["Rust + WASM 读者"] --> B["前端工程师<br/>已会 JS/TS<br/>想加性能优化能力"]
  A --> C["Rust 工程师<br/>已会 Rust<br/>想把后端能力上 Web"]
  A --> D["架构师/技术负责人<br/>需要决策<br/>是否引入 WASM"]
  style B fill:#10b981,color:#333
  style C fill:#6366f1,color:#fff
  style D fill:#f59e0b,color:#333

每类读者的痛点不同:

  • 前端工程师:Rust 是新语言,要先建立信心,再学集成
  • Rust 工程师:WASM 边界、JS 生态是盲点,浏览器调试体验需要重建
  • 架构师:技术细节其次,决策框架和案例数据最重要

1.8.2 三条阅读路径

路径一:前端工程师快速入门(建议 2 周)

ch1 (本章)  → ch5 (Rust → WASM)  → ch6 (wasm-bindgen)
       ↓
ch7 (类型映射) → ch8 (wasm-pack) → ch16 (浏览器集成)
       ↓
ch11 (内存通信) → ch9 (体积优化) → ch10 (性能)

跳过 ch2-4(规范层)、ch12-15(WASI 与组件模型)。这条路径覆盖"在 Vite/Webpack 项目中引入 WASM 模块"的全部知识。

路径二:Rust 工程师 Web 化(建议 3 周)

ch1 → ch2-4(理解 WASM 规范)
   ↓
ch5-8(工具链全套)
   ↓
ch11 (内存) → ch16 (浏览器) → ch18 (可观测性)
   ↓
ch19 (生产案例) → ch20 (设计模式)

跳过 Rust 基础类内容(不需要),但补足 WASM 规范和浏览器集成知识。

路径三:架构师决策路径(建议 3 天)

ch1 → ch10 (性能上限) → ch19 (生产案例)
   ↓
ch20 (设计决策) → ch16/17(按需选浏览器或服务器)

聚焦数据和决策框架——ch10 的性能数据告诉你 WASM 能做到什么,ch19 的案例告诉你别人怎么做,ch20 给出系统化决策。

1.8.3 章节难度分级

graph LR
  subgraph "入门(必读)"
    A1[ch1 - WHY]
    A2[ch5 - Rust→WASM]
    A3[ch6 - wasm-bindgen]
  end
  subgraph "进阶(按需)"
    B1[ch7 - 类型映射]
    B2[ch9 - 体积]
    B3[ch10 - 性能]
    B4[ch11 - 内存]
    B5[ch16 - 浏览器]
  end
  subgraph "深入(专题)"
    C1[ch2-4 - 规范]
    C2[ch12-15 - WASI/组件]
    C3[ch17 - 服务端]
    C4[ch18 - 可观测]
    C5[ch19-20 - 实战]
  end
  A3 --> B1
  A3 --> B5
  B5 --> C1
  B5 --> C3
  style A1 fill:#10b981,color:#333
  style A2 fill:#10b981,color:#333
  style A3 fill:#10b981,color:#333

入门部分是所有读者的共同起点——其余章节按兴趣和需求选择。

1.8.4 配套实践

读书不动手等于没读。每章末提到的代码示例、性能基准、案例代码,建议在本地真实跑一遍:

章节 实践项目
ch5-8 写一个完整的 wasm-pack 项目并发布到 npm
ch9-10 跑正文里的体积/性能基准,复现数据
ch11 实现一个零拷贝的图像处理函数
ch12-15 用 Wasmtime 跑一个 WASI 组件
ch16 把上述项目集成到 Vite/React 应用中

这套实践覆盖从"hello world"到"生产级集成"的全过程。完成后已经具备生产 WASM 项目的能力。

1.8.5 学习曲线预期

不要期待"一周入门"——Rust + WASM 涉及多个领域知识:

阶段 时间(经验估计) 标志
知道概念 2-3 天 能解释 WASM 是什么、为什么
写出 hello world 1 周 完成 wasm-pack 起步项目
解决简单问题 2-3 周 能用 WASM 优化一个具体函数
产品级集成 1-3 月 能在生产中部署、监控、迭代
深度优化 半年+ 理解 LLVM 后端、能做极致优化

学习节奏应该匹配实际工程需求——不需要一次性掌握所有内容。先学解决当前问题的部分,再随项目复杂度逐步深入。

1.9 WASM 与其他新兴 Web 技术的协同

WASM 不是孤立技术——它是 Web 平台演进的一部分。与 WebGPU、WebTransport、WebCodecs 等同代技术配合,能解锁原本不可能的 Web 应用形态。

1.9.1 现代 Web 平台的能力栈

graph TD
  A["现代 Web 能力"] --> B["计算"]
  A --> C["GPU"]
  A --> D["网络"]
  A --> E["媒体"]
  A --> F["IO"]
  B --> B1["WASM"]
  C --> C1["WebGPU / WebGL"]
  D --> D1["WebTransport / WebRTC"]
  E --> E1["WebCodecs / WebAudio"]
  F --> F1["File System Access / OPFS"]
  style B fill:#10b981,color:#333
  style C fill:#6366f1,color:#fff
  style D fill:#f59e0b,color:#333
  style E fill:#dc2626,color:#fff

每项技术解决特定瓶颈——WASM 是计算层,与其他层协作才能构建完整应用。

1.9.2 WASM + WebGPU:计算密集应用

flowchart LR
  A["WASM 数据准备"] --> B["WebGPU compute shader"]
  B --> C["GPU 大规模并行"]
  C --> D["WASM 后处理"]
  style A fill:#10b981,color:#333
  style C fill:#6366f1,color:#fff

WASM 处理 CPU 密集的"控制流 + 数据准备",WebGPU 处理"高度并行的计算核心"。典型场景:

  • 图像处理:WASM 预处理 → WebGPU 滤镜 → WASM 编码
  • ML 推理:WASM 数据预处理 → WebGPU 矩阵乘 → WASM 后处理
  • 游戏物理:WASM 游戏逻辑 → WebGPU 粒子模拟

1.9.3 WASM + WebCodecs:实时媒体

flowchart TD
  A["视频流"] --> B["WebCodecs 解码"]
  B --> C["WASM 滤镜处理"]
  C --> D["WebCodecs 编码"]
  D --> E["WebTransport 传输"]
  style C fill:#10b981,color:#333

WebCodecs(Chrome 94+)让浏览器能直接处理 H.264 / AV1 等编码——WASM 处理中间的滤镜或分析。这种组合是浏览器视频会议、实时滤镜等应用的基础。

1.9.4 WASM + WebTransport:低延迟网络

WebTransport 提供基于 HTTP/3 的低延迟双向通信,是 WebSocket 之外的另一种选择:

graph LR
  A["客户端 WASM"] --> B["WebTransport 连接"]
  B --> C["服务端"]
  D["WebTransport 能力"] --> E["双向流(可靠)"]
  D --> F["单向流(可靠)"]
  D --> G["数据报(不可靠)"]
  style B fill:#10b981,color:#333

WASM 应用通过 WebTransport 与服务端通信——相比 WebSocket 的单条有序流,它建立在 HTTP/3 之上,支持多路独立的流和不可靠数据报,能避开队头阻塞。这是浏览器游戏、实时协作的关键基础。

1.9.5 WASM + File System Access:本地文件操作

window.showDirectoryPicker()(File System Access API,目前只有 Chromium 系浏览器支持)让网页在用户授权后读写本地目录;navigator.storage.getDirectory() 则是各浏览器都支持的源私有文件系统(OPFS),不对应用户可见的文件。配合 WASM,浏览器应用能像桌面工具一样操作大文件:

// 用户选择文件夹(File System Access API)
const dir = await window.showDirectoryPicker({ mode: 'readwrite' });
const handle = await dir.getFileHandle('data.bin');

// 读:handle → File → ArrayBuffer
const buf = new Uint8Array(await (await handle.getFile()).arrayBuffer());

// 交给 WASM 处理
const result = wasm.process(buf);

// 写:必须先拿到 writable 流
const writable = await handle.createWritable();
await writable.write(result);
await writable.close();

这类能力是"桌面级"浏览器应用直接打开、保存本地文件的基础。

1.9.6 协同应用的代表案例

应用 核心技术栈
Figma WASM + WebGL,已在支持的浏览器上改用 WebGPU 渲染(Figma 博客)
TensorFlow.js WASM / WebGL / WebGPU 三种后端
StackBlitz(WebContainers) 基于 WASM 的浏览器内"操作系统"跑 Node.js + Service Worker 映射虚拟网络栈(StackBlitz 博客)

每个应用都不只用 WASM——而是 WASM + 多个 Web 平台 API 的组合。

1.9.7 协同的工程模式

flowchart TD
  A["WASM 协同其他 API"] --> B["1. 各做擅长事"]
  A --> C["2. 减少跨边界"]
  A --> D["3. fallback 准备"]
  B --> B1["WASM 计算 / WebGPU 并行 / WebCodecs 编解码"]
  C --> C1["数据流尽量在 WASM 内"]
  D --> D1["新 API 浏览器支持率不高<br/>必须 fallback"]
  style A fill:#6366f1,color:#fff

每条都对应工程实践:

  • 各做擅长事:不要用 WASM 做 GPU 该做的事,反之亦然
  • 减少跨边界:每次 WASM ↔ JS API 切换都有开销
  • fallback:WebGPU 等新 API 尚未在全部浏览器落地,必须有降级路径

1.9.8 浏览器支持率(2026)

graph LR
  A["Web API 落地程度"] --> B["WASM: 全部主流浏览器,2017 年起"]
  A --> C["WebGL: 全部主流浏览器"]
  A --> D["OPFS: 全部主流浏览器,较新版本"]
  A --> E["WebCodecs: Chromium 系领先,其余跟进中"]
  A --> F["WebGPU / WebTransport: 尚未全面覆盖"]
  style B fill:#10b981,color:#333
  style E fill:#f59e0b,color:#333
  style F fill:#dc2626,color:#fff

WASM 是这一批里覆盖最早、最稳的之一——2017 年内就先后进入四大浏览器,而 WebGPU、WebTransport 等仍在普及。具体支持情况随时在变,动手前应查 MDN 或 caniuse 的当期数据;设计协同应用时的铁律是:依赖尚未全面覆盖的 API 的功能必须有降级路径。

1.10 WASM 的常见误解

任何热门技术都伴随大量误解——WASM 也不例外。在深入后续章节之前,澄清这些误解能避免走错路。

1.10.1 误解清单

graph TD
  A["WASM 常见误解"] --> B["误解 1:WASM 一定比 JS 快"]
  A --> C["误解 2:WASM 能取代 JS"]
  A --> D["误解 3:WASM 只能做计算"]
  A --> E["误解 4:WASM 体积小"]
  A --> F["误解 5:WASM 学习简单"]
  A --> G["误解 6:WASM 是新技术"]
  A --> H["误解 7:WASM = Rust"]
  style A fill:#f59e0b,color:#333

1.10.2 误解 1:WASM 一定比 JS 快

事实:WASM 在特定场景比 JS 快(计算密集、SIMD 友好),其他场景可能更慢。

graph LR
  A["场景"] --> B["WASM 更快"]
  A --> C["JS 更快"]
  A --> D["差不多"]
  B --> B1["纯计算 / SIMD / 算法"]
  C --> C1["DOM 操作 / JSON.parse / 浏览器引擎优化的 API"]
  D --> D1["简单业务逻辑"]
  style B fill:#10b981,color:#333
  style C fill:#dc2626,color:#fff

V8 对 JS 有近二十年的优化积累——某些场景(如 JSON 解析)WASM 反而慢。盲目用 WASM 可能让性能更差。

1.10.3 误解 2:WASM 能取代 JS

事实:WASM 是 JS 的补充,不是替代。WASM 不能直接操作 DOM,所有 UI 操作都要经过 JS。

graph TD
  A["浏览器应用架构"] --> B["UI / DOM 层"]
  A --> C["业务逻辑层"]
  A --> D["计算引擎层"]
  B --> B1["JS(必须)"]
  C --> C1["JS / TS / WASM 都可"]
  D --> D1["WASM(推荐)"]
  style B1 fill:#10b981,color:#333
  style D1 fill:#6366f1,color:#fff

目前没有让 WASM 直接操作 DOM 的标准化提案;已落地的方向是降低调用 JS 与 Web API 的成本(例如 reference types 让 WASM 可以直接持有 JS 对象引用)。可以预见相当长时间内 JS 仍是 UI 层的主流。

1.10.4 误解 3:WASM 只能做计算

事实:早期 WASM 确实主要用于计算——但 WASI Preview 2 + 组件模型让 WASM 能做的事大幅扩展:

  • HTTP 服务(wasi:http)
  • 数据库/分析引擎的用户自定义函数(各家自建宿主)
  • 服务网格 filter(Envoy 的 Proxy-Wasm)
  • 智能合约(Polkadot、NEAR)
  • 边缘计算函数(Cloudflare Workers、Fastly Compute)

2026 年的 WASM 远不止"浏览器内的计算引擎"。

1.10.5 误解 4:WASM 体积小

事实:Rust → WASM 产物一旦用到格式化、集合、序列化等标准库设施,未经优化的体积很容易比同等功能的 JS 库大得多。

graph LR
  A["体积对比"] --> B["未优化 Rust WASM<br/>常常偏大"]
  A --> C["优化后 WASM<br/>明显缩小"]
  A --> D["同等功能的 JS 库<br/>往往更小"]
  style B fill:#dc2626,color:#fff
  style C fill:#f59e0b,color:#333
  style D fill:#10b981,color:#333

WASM 体积优化(第 9 章)是必修课——否则用户加载体验会被 .wasm 拖累。

1.10.6 误解 5:WASM 学习简单

事实:WASM 工程涉及多个领域——Rust 语言 + WASM 规范 + JS 互操作 + 浏览器/服务端运行时。要达到能独立交付生产代码的水平,通常以数月计(作者经验估计)。

graph TD
  A["WASM 学习曲线"] --> B["第 1 周"]
  A --> C["1 月"]
  A --> D["3 月"]
  A --> E["6 月"]
  B --> B1["跑 hello world"]
  C --> C1["小项目可工作"]
  D --> D1["生产级中型项目"]
  E --> E1["架构 / 性能调优"]
  style A fill:#6366f1,color:#fff

上图的时间刻度只是经验估计;要点是 WASM 工程等于 JS 与 Rust 两套技能的叠加——别低估投入。

1.10.7 误解 6:WASM 是新技术

事实:WASM MVP 2017 年发布——已经 9 年。在浏览器中已经成熟。

timeline
    title WASM 成熟度
    2017 : MVP 发布<br/>四浏览器支持<br/>Figma 等产品上线 WASM 版
    2019 : Core 1.0 成为 W3C 推荐标准
    2022 : Wasm 2.0 基本完成(含 SIMD)
    2024 : 组件模型 + WASI P2
    2026 : 服务端生态成熟

把 WASM 当"实验技术"是过时的认知——它早已是 W3C 正式标准,也是边缘计算平台的常见选项。

1.10.8 误解 7:WASM = Rust

事实:WASM 是字节码格式,与 Rust 无关。任何语言只要有 WASM 编译器都能产出 .wasm。

graph TD
  A[".wasm 二进制"] --> B["Rust"]
  A --> C["C / C++(Emscripten)"]
  A --> D["AssemblyScript(TS-like)"]
  A --> E["Go(限制较多)"]
  A --> F["Python(Pyodide)"]
  A --> G["Java(TeaVM)"]
  style B fill:#10b981,color:#333
  style A fill:#6366f1,color:#fff

Rust 是编译到 WASM 最常用的语言之一,但不是唯一。本专栏聚焦 Rust + WASM,但记住"WASM != Rust"——其他语言也是合法选择。

1.11 WASM 的成本-收益判断框架

要不要在项目中引入 WASM——这是技术决策者面临的高频问题。本节提供一套量化的判断框架,避免凭直觉决策。

1.11.1 引入 WASM 的成本结构

graph TD
  A["引入 WASM 的成本"] --> B["人力成本"]
  A --> C["工具链成本"]
  A --> D["运维成本"]
  A --> E["机会成本"]
  B --> B1["学习曲线以数月计<br/>按团队薪资折算"]
  C --> C1["多一条 Rust 工具链<br/>构建时间变长"]
  D --> D1["新监控/部署链路"]
  E --> E1["不做新功能的代价"]
  style A fill:#dc2626,color:#fff

每项成本都要按自己团队的真实数字量化——本节只给出成本的结构,具体金额取决于团队规模、薪资水平和项目复杂度,任何"通用数字"都是误导。

1.11.2 WASM 的潜在收益

graph TD
  A["WASM 收益"] --> B["性能"]
  A --> C["用户体验"]
  A --> D["业务"]
  A --> E["技术"]
  B --> B1["计算密集路径的耗时<br/>可能降低一个量级"]
  C --> C1["FCP/LCP 改善"]
  D --> D1["转化率/留存上升"]
  E --> E1["代码复用 / 多语言"]
  style A fill:#10b981,color:#333

关键:性能改善 ≠ 业务收益。性能提升必须转化为用户感知和业务指标——否则只是技术自嗨。

1.11.3 ROI 计算框架

ROI = (业务收益 × 12 月 - 投入成本) / 投入成本

这个公式的每一项都必须来自你自己的数据,而不是别处抄来的行业均值:

graph LR
  A["填空题"] --> B["投入:工程师人数 × 月数<br/>× 你的人月成本"]
  A --> C["链路:耗时下降 → 留存/转化变化<br/>必须有 A/B 实验支撑"]
  A --> D["收益:转化变化 × 客单价 × 流量"]
  A --> E["ROI = (年化收益 - 投入) / 投入"]
  style E fill:#10b981,color:#333

关键不在算出的那个百分比,而在中间那一步——"耗时下降"到"业务指标变化"的链路是否真的成立。这一步没有实验数据支撑时,ROI 计算只是把主观判断包装成了数字。

1.11.4 何时值得优先考虑 WASM

flowchart TD
  A["哪些场景值得优先考虑 WASM?"] --> B["1. 性能瓶颈在 CPU 密集计算"]
  A --> C["2. 现有 C/C++ 代码库需要 Web 化"]
  A --> D["3. 多语言团队协作(组件模型)"]
  A --> E["4. 边缘函数 + 微秒级冷启动"]
  A --> F["5. 沙箱执行第三方代码"]
  style A fill:#10b981,color:#333

这 5 类场景是 WASM 最能体现价值的地方——其他方案往往做不到或代价更高。

1.11.5 何时不该用 WASM

flowchart TD
  A["哪些场景不该用 WASM?"] --> B["1. 简单 CRUD 应用"]
  A --> C["2. UI 操作密集"]
  A --> D["3. 团队全是 JS/TS 工程师"]
  A --> E["4. 项目周期很短"]
  A --> F["5. 性能不是瓶颈"]
  style A fill:#dc2626,color:#fff

强行引入只会增加复杂度——业务效果反而下降。

1.11.6 决策矩阵

flowchart TD
  A["WASM 决策"] --> B{"性能是核心瓶颈?"}
  B -->|是| C{"团队懂 Rust?"}
  B -->|否| D["不用"]
  C -->|是| E["用"]
  C -->|否| F{"愿意投入学习成本?"}
  F -->|是| E
  F -->|否| D
  style E fill:#10b981,color:#333
  style D fill:#dc2626,color:#fff

简单两步决策:性能瓶颈 + 团队能力。两个都满足才用 WASM。

1.11.7 渐进式引入策略

如果决定引入 WASM,渐进比一步到位更稳:

flowchart LR
  A["阶段 1<br/>1 个月"] --> B["小型 PoC<br/>1 名工程师"]
  B --> C["阶段 2<br/>3 个月"]
  C --> D["生产级单一功能<br/>2-3 工程师"]
  D --> E["阶段 3<br/>6 个月+"]
  E --> F["核心模块 WASM 化<br/>整个团队"]
  style A fill:#10b981,color:#333
  style E fill:#6366f1,color:#fff

各阶段时长是经验估计,按团队情况调整;关键是每阶段都有明确产出和退出标准——某一阶段失败可以止损,不会全军覆没。

1.11.8 给非技术决策者的简化指南

flowchart TD
  A["产品 / 技术管理者"] --> B{"业务关心什么?"}
  B --> C["首屏速度"]
  B --> D["用户互动"]
  B --> E["计算密集功能"]
  C --> F["WASM 通常无帮助"]
  D --> G["视情况"]
  E --> H["考虑 WASM"]
  style F fill:#dc2626,color:#fff
  style H fill:#10b981,color:#333

给非工程岗位的决策者一个简化版——大多数业务关心的"快"是首屏快、互动快——这些不是 WASM 的强项。WASM 强在"做以前做不到的复杂功能"。

1.12 WASM 生态地图

读完技术细节前,先建立 WASM 生态的全景认知——理解谁在推进、有哪些资源、向哪里寻求帮助。这是高效学习的基础。

1.12.1 核心组织与项目

graph TD
  A["WASM 生态核心"] --> B["W3C WebAssembly CG"]
  A --> C["Bytecode Alliance"]
  A --> D["主流浏览器厂商"]
  B --> B1["规范制定<br/>提案审议"]
  C --> C1["Wasmtime / WAMR<br/>wit-bindgen / wasm-tools"]
  D --> D1["Google V8 / Mozilla SpiderMonkey<br/>Apple JSC"]
  style A fill:#10b981,color:#333

每个组织的角色不同——理解后能精准定位信息来源。

1.12.2 关键工具栈

graph TD
  A["WASM 工具链"] --> B["编译"]
  A --> C["运行时"]
  A --> D["开发"]
  A --> E["分析"]
  B --> B1["rustc / Emscripten / AssemblyScript"]
  C --> C1["Wasmtime / Wasmer / WAMR / V8"]
  D --> D1["wasm-pack / wit-bindgen / cargo-component"]
  E --> E1["wasm-tools / Binaryen / twiggy"]
  style A fill:#6366f1,color:#fff

每个工具有特定用途——按需选择。

1.12.3 学习资源

资源 类型 适用
WebAssembly.org 官网 入门 + 规范索引
webassembly.github.io/spec 规范 深入
wasm-bindgen.github.io/wasm-bindgen wasm-bindgen 官方指南 Rust + WASM
bytecodealliance.org 组织主页 工具链消息

老教程常指向的 rustwasm.github.io 系列文档来自已解散的 Rust and WebAssembly 工作组:该工作组 2024 年归档,rustwasm GitHub 组织于 2025 年归档,wasm-bindgen 已迁到独立的 wasm-bindgen 组织继续维护(Inside Rust 博客)。

1.12.4 开源项目集合

graph TD
  A["awesome-wasm 类清单"] --> B["awesome-wasm"]
  A --> C["awesome-rust-and-webassembly(已归档)"]
  A --> D["awesome-wasi"]
  B --> B1["WASM 全生态"]
  C --> C1["Rust + WASM 项目"]
  D --> D1["WASI 相关"]
  style A fill:#6366f1,color:#fff

每个 awesome 列表都是社区维护的资源汇总——快速浏览能发现工具和库。

1.12.5 标杆产品(值得研究的)

graph TD
  A["值得研究的 WASM 产品"] --> B["浏览器端"]
  A --> C["服务器端"]
  A --> D["边缘"]
  B --> B1["Figma / Photoshop Web / Google Earth"]
  C --> C1["Wasmtime / Spin / wasmCloud"]
  D --> D1["Cloudflare Workers / Fastly Compute"]
  style A fill:#10b981,color:#333

研究这些产品的源码或公开博客——比读教程能更快建立工程直觉。

1.12.6 跟踪规范进展

flowchart TD
  A["规范进展跟踪"] --> B["GitHub WebAssembly/proposals"]
  A --> C["CG 会议纪要(WebAssembly/meetings)"]
  style A fill:#10b981,color:#333

具体提案的进展看 GitHub——每个提案有自己的 repo + issue tracker。重要提案订阅 notifications,第一手了解动态。

1.13 Rust + WASM 的局限性

公正起见,Rust + WASM 也有不擅长的场景。这些局限性不是 Rust 的问题,也不是 WASM 的问题,而是两者组合后的结构性限制。

不适合 DOM 操作

WASM 模块无法直接操作 DOM——WASM MVP 的指令集中没有 DOM 相关的操作。每次 DOM 操作都需要经过 JS 中转:WASM 调用导出的 JS 函数 → JS 调用 DOM API → 返回结果到 WASM。单次跨边界调用本身很轻,但高频操作会把它累积成瓶颈——第 11 章会给出这笔账的算法。

经验法则:DOM 操作用 JS,计算密集任务用 WASM。Yew 和 Leptos 等 Rust 前端框架的做法是:框架层在 WASM 中完成(Yew 做 virtual DOM diff,Leptos 用细粒度响应式直接定位要改的节点),最终 DOM 操作仍经 JS 胶水调用。本专栏第 16 章会展开这套分工在 Yew、Leptos 中的具体形态。

冷启动开销

WASM 模块需要经过解码→验证→编译→实例化四个阶段。即使使用流式编译(streaming compilation,V8 在下载 .wasm 的同时开始编译),MB 量级的 .wasm 文件在移动端仍会带来可感知的冷启动延迟——低端设备上尤其明显。对于首屏渲染关键路径上的代码,这个延迟往往不可接受。第 4 章会拆解这四个阶段各自的成本。

解决方案通常是代码分割和懒加载——只首屏加载必要的 WASM 模块,其余按需加载。wasm-pack 的 --target web 模式支持 ES Module 的 import() 动态加载,可以和 Vite 的代码分割策略配合。这在《Vite 设计与实现》专栏第 17 章"Web Worker 与特殊资源"中有详细讨论。

调试体验不如原生

浏览器的 DevTools 对 WASM 的调试支持在持续改善,但仍不如 JavaScript。断点可以设在 WASM 函数上,变量可以在 DWARF 调试信息的帮助下以 Rust 类型展示,但调用栈混合了 JS 和 WASM 帧,容易让开发者困惑。Chrome DevTools 通过官方的 C/C++ DevTools Support 扩展消费 DWARF 信息,从而按源码行和 Rust 类型展示。第 18 章会展开这套调试链路。

工具链版本的碎片化

wasm-bindgen 的 CLI 和 crate 之间靠一个 schema 版本号对齐:crate 把 wasm_bindgen_shared::SCHEMA_VERSION(📄 crates/shared/src/lib.rs,0.2.118 里它的值就是 "0.2.118")写进 .wasm,CLI 加载时逐字比对,不一致就直接 bail! 报错,并提示你用 cargo update -p wasm-bindgen --precise 或重装对应版本的 CLI(📄 crates/cli-support/src/wit/mod.rs)。这在 CI/CD 环境中经常出问题:cargo install wasm-bindgen-cli --version X.Y.Z 中的版本号必须和 Cargo.lock 里实际解析出的 wasm-bindgen 版本完全一致(Cargo.toml 里的 "0.2" 只是版本要求)。wasm-pack 内部处理了这个匹配问题——它读 Cargo.lock 取出 wasm-bindgen 的版本再准备对应的 CLI(📄 wasm-pack src/lockfile.rs、src/command/build.rs),但如果直接使用 wasm-bindgen CLI,需要注意版本对齐。

1.14 全专栏路线图

本专栏按"规范 → 工具链 → 性能 → 超越浏览器 → 工程实践"的五层结构展开,每一层都建立在前一层的基础上:

graph TD
    subgraph "第一部分:规范层(第2-4章)"
        A1[第2章 WASM 规范:二进制格式与指令集]
        A2[第3章 线性内存与表]
        A3[第4章 虚拟机:验证、编译与执行]
        A1 --> A2 --> A3
    end
    subgraph "第二部分:工具链层(第5-8章)"
        B1[第5章 Rust → wasm32 代码生成]
        B2[第6章 wasm-bindgen]
        B3[第7章 类型映射与胶水代码]
        B4[第8章 wasm-pack]
        B1 --> B2 --> B3 --> B4
    end
    subgraph "第三部分:性能层(第9-11章)"
        C1[第9章 体积优化]
        C2[第10章 运行时性能]
        C3[第11章 内存与通信开销]
        C1 --> C2 --> C3
    end
    subgraph "第四部分:超越浏览器(第12-15章)"
        D1[第12章 WASI]
        D2[第13章 WASI Preview 2]
        D3[第14章 组件模型]
        D4[第15章 wit-bindgen]
        D1 --> D2 --> D3 --> D4
    end
    subgraph "第五部分:工程实践(第16-20章)"
        E1[第16章 浏览器集成]
        E2[第17章 服务器端 WASM]
        E3[第18章 可观测性]
        E4[第19章 生产案例]
        E5[第20章 设计模式]
        E1 --> E2 --> E3 --> E4 --> E5
    end
    A3 --> B1
    B4 --> C1
    C3 --> D1
    D4 --> E1
    style A1 fill:#6366f1,color:#fff
    style B1 fill:#22d3ee,color:#333
    style C1 fill:#f59e0b,color:#333
    style D1 fill:#10b981,color:#333
    style E1 fill:#dc2626,color:#fff

每一层都建立在前一层的基础上:不理解线性内存(第 3 章),就无法理解 wasm-bindgen 的内存分配策略(第 6 章);不理解 wasm-bindgen 的类型映射(第 7 章),就无法评估 Guest-Host 通信的开销(第 11 章);不理解组件模型(第 14 章),就无法设计可组合的插件架构(第 17 章)。

下一章,我们从最底层开始——W3C WebAssembly 规范定义了什么样的二进制格式、指令集和类型系统。