WebAssembly(简称 Wasm)是一种面向虚拟机的低级二进制指令格式,常见用法是把 C、C++、Rust、Go、C# 等语言编译成 .wasm 文件,再交给浏览器或服务端 Wasm 运行时执行。
它不是为了让开发者手写一门新语言,也不是为了替代 JavaScript。更准确的说法是:Wasm 给 Web 和服务端提供了一个统一、紧凑、可沙箱隔离、接近原生速度的编译目标。
Wasm 3.0 的意义在于,它不再只围绕“让 C/C++ 跑进浏览器”这个早期目标演进,而是开始补齐高级语言、服务端沙箱、跨平台运行时所需要的关键能力。
| 特性 | 改变了什么 | 适合解决的问题 |
|---|---|---|
| 64 位地址空间 | 内存和表可以使用 64 位地址,不再局限于 32 位寻址 | 大数据处理、数据库、图形计算、科学计算等需要超过 4 GB 地址空间的场景 |
| 多内存 | 一个模块可以定义或导入多个 memory 对象 | 敏感数据隔离、模块静态合并、多缓冲区管理、多级内存模拟 |
| 垃圾回收(GC) | Wasm 运行时可以直接管理对象、结构体、数组等堆数据 | Java、Kotlin、Scala、OCaml 等带对象模型的高级语言更容易编译到 Wasm |
| 强类型引用 | 引用可以绑定更具体的堆类型,支持子类型、递归类型、函数引用精化 | 降低运行时类型检查成本,让编译器生成更安全、更高效的代码 |
| 尾调用 | 支持不额外增长调用栈的尾调用语义 | 函数式语言、递归算法、解释器实现 |
| 异常处理 | Wasm 内部支持 throw / catch 类似机制 | C++、Java、C# 等语言移植时不用再绕到宿主语言模拟异常 |
| 放宽向量指令 | SIMD 指令允许在不同硬件上选择更合适的实现 | 音视频处理、图像处理、机器学习推理等计算密集任务 |
| 确定性执行配置 | 对浮点 NaN、放宽 SIMD 等可能不确定的行为给出标准化约束 | 区块链、可重放计算、状态同步、多端一致性校验 |
| 文本格式注解 | .wat 文本格式可表达自定义 section 的语义 | 工具链、调试器、编译器扩展更容易维护 |
| JavaScript 字符串内建 | Wasm 和 JavaScript 交互时可更直接地处理 JS 字符串 | 前端混合应用中减少字符串桥接成本 |
| SpecTec 规格工具链 | 规范由新的规格工具链生成 | 标准维护、验证和后续扩展更稳定 |
这些能力可以归成三条主线:
flowchart TD
A[Wasm 3.0] --> B[更大的运行边界]
A --> C[更友好的语言生态]
A --> D[更可控的执行语义]
B --> B1[64 位地址空间]
B --> B2[多内存]
B --> B3[放宽 SIMD]
C --> C1[GC]
C --> C2[强类型引用]
C --> C3[尾调用]
C --> C4[异常处理]
D --> D1[确定性执行]
D --> D2[文本注解]
D --> D3[规范工具链]
前端可以用 Wasm 承接浏览器里的计算密集模块,后端可以用 Wasm 沙箱运行第三方逻辑,平台团队可以把它作为统一执行层的一部分。AI 推理、大数据处理、边缘计算也会受益于更大的内存空间和更稳定的执行语义。
Wasm 解决的不是“没有 JavaScript”,而是“JavaScript 不适合所有计算”
HTML、CSS 和 JavaScript 已经能构建绝大多数 Web 应用。问题出现在另一些场景:音视频编解码、图像滤镜、3D 游戏、CAD、在线设计工具、机器学习推理、压缩解压、加密计算。这些任务通常有几个特点:
| 特点 | 对 JavaScript 的影响 | Wasm 的价值 |
|---|---|---|
| CPU(中央处理器)计算密集 | 大量循环、算术、数据转换容易占满主线程 | 可用 C/C++/Rust 等语言写核心算法,生成更紧凑的低级指令 |
| 需要复用已有代码 | 很多成熟库本来就是 C/C++ 写的 | 可通过 Emscripten 等工具链迁移到 Web |
| 对内存布局敏感 | JavaScript 对象模型和垃圾回收不容易精确控制内存布局 | Wasm 使用线性内存,更接近系统语言的模型 |
| 需要沙箱运行 | 第三方代码直接运行在平台内风险高 | Wasm 默认隔离,能力需要显式授予 |
| 需要跨平台分发 | 不同 CPU、操作系统、浏览器环境差异大 | .wasm 作为统一二进制格式,由不同运行时适配底层平台 |
Wasm 在浏览器里的定位通常是“计算内核”,而不是“页面框架”。页面结构、状态管理、DOM 交互、网络请求仍然适合用 JavaScript 和前端框架处理;真正消耗 CPU 的算法模块,才适合拆出来交给 Wasm。
从 asm.js 到 WebAssembly:浏览器里跑 C/C++ 的探索
Wasm 的技术路线可以从两个方向理解:Mozilla 的 asm.js 和 Google 的 Native Client。
Emscripten 和 asm.js
Emscripten 是一个基于 LLVM 的工具链,它可以把 C/C++ 编译成 asm.js 或 Wasm。LLVM IR(Intermediate Representation,中间表示)是编译器内部使用的一种中间层,只要某门语言能编译到 LLVM IR,理论上就有机会继续转成 Wasm。
flowchart LR
A[C/C++/Rust 等源码] --> B[Clang 或语言编译器]
B --> C[LLVM IR]
C --> D[Emscripten / Binaryen]
D --> E[asm.js]
D --> F[WebAssembly]
E --> G[浏览器 JavaScript 引擎]
F --> H[浏览器 Wasm 虚拟机]
asm.js 本质上仍然是 JavaScript,只是它是一种非常受限、面向编译器生成的 JavaScript 子集。
例如,一个简单的 C 函数:
int add_one(int i) {
return i + 1;
}
size_t strlen_like(char *ptr) {
char *curr = ptr;
while (*curr != 0) {
curr++;
}
return curr - ptr;
}
转换成 asm.js 后,代码会变得很不“手写友好”:
function add_one(i) {
i = i | 0;
return (i + 1) | 0;
}
function strlen_like(ptr) {
ptr = ptr | 0;
var curr = 0;
curr = ptr;
while ((MEM8[curr >> 0] | 0) != 0) {
curr = (curr + 1) | 0;
}
return (curr - ptr) | 0;
}
这里频繁出现的 |0 是为了把值压成 32 位整数,让 JavaScript 引擎更容易进行类型特化优化。MEM8 这样的数组则承担了线性内存的角色,用数组模拟一块连续堆内存。
asm.js 之所以比普通 JavaScript 更容易跑快,核心不是什么“跳过语法分析”,也不是“用 WebGL 让 GPU 执行 JavaScript”。真正的原因是它减少了运行时的不确定性:
- 类型更稳定,编译器不用频繁猜测变量类型。
- 内存模型更接近系统语言,很多对象分配和垃圾回收压力被绕开。
- 代码结构更规律,JavaScript 引擎更容易做 Ahead-of-Time(AOT,提前编译)或 Just-in-Time(JIT,即时编译)优化。
- 算法热点集中在循环、算术、数组访问、函数调用这些容易优化的指令上。
动态语言慢的根源之一,是很多事情要在运行时决定。变量是什么类型、函数到底跳到哪里、对象形状是否变化、是否触发垃圾回收,这些都会让引擎生成额外检查逻辑。asm.js 通过严格约束写法,把一部分动态决议提前固定下来。
Native Client 为什么没有成为通用方案
Google Native Client(NaCl)也试图让 C/C++ 以接近原生应用的性能跑在浏览器中。它强调沙箱隔离,并且在安全上做了大量设计,例如双层沙箱、静态代码分析、内存隔离等。
NaCl 的技术问题不只是性能和安全,而是生态。它主要绑定 Chrome,其他浏览器没有足够动力跟进。Web 平台需要的是跨浏览器标准,不是某个浏览器厂商独占的运行机制。
Wasm 后来成为主流路线,关键原因正是它进入了标准化路径,并且得到主流浏览器共同支持。
Wasm 的工作机制:高级语言到可移植二进制
Wasm 有两种常见表现形式:
| 格式 | 扩展名 | 用途 |
|---|---|---|
| 二进制格式 | .wasm | 给浏览器或运行时加载执行,体积紧凑 |
| 文本格式 | .wat | 给人阅读、调试、学习指令结构 |
开发者通常不会手写 .wasm,而是这样工作:
flowchart LR
A[高级语言源码] --> B[语言编译器]
B --> C[.wasm 二进制模块]
C --> D[Wasm 运行时校验]
D --> E[编译为本机机器码]
E --> F[在沙箱中执行]
G[JavaScript / WASI / 宿主程序] --> C
C --> G
Wasm 模块本身不能随意访问外部世界。它能用哪些函数、能读写哪些内存、能访问哪些文件或网络资源,都要由宿主环境显式提供。这也是 Wasm 能作为插件沙箱和服务端隔离层的重要原因。
在浏览器中,宿主环境通常是 JavaScript 和浏览器 API(Application Programming Interface,应用程序编程接口)。在服务端,宿主环境可能是 Wasmtime、Wasmer、WasmEdge 这类运行时。
Wasm 为什么通常比 JavaScript 更快
Wasm 的性能优势主要来自四个方面。
| 来源 | 解释 |
|---|---|
| 体积更小 | .wasm 是紧凑二进制格式,比等价 JavaScript 更容易下载和缓存 |
| 解析更快 | 浏览器不需要像解析 JavaScript 那样处理复杂语法和动态语言特性 |
| 指令更低级 | Wasm 指令更接近机器模型,可以表达 64 位整数、显式加载/存储、SIMD 等能力 |
| 编译链更可控 | C/C++/Rust 编译器可以在生成 Wasm 前做大量静态优化 |
但不能把 Wasm 简单理解成“必然比 JavaScript 快很多”。
如果 JavaScript 写得非常底层,例如大量使用 ArrayBuffer、TypedArray,避免对象分配和垃圾回收,把热点逻辑收敛到循环和数值计算里,它也能达到很高性能。浏览器 JavaScript 引擎已经优化了很多年,普通算术、局部变量、紧凑数组访问并不慢。
一个典型例子是二维向量平均长度计算。普通 TypeScript 可能会用对象数组:
type Vec2 = { x: number; y: number };
function avgLen(vecs: Vec2[]): number {
let total = 0;
for (const vec of vecs) {
total += Math.sqrt(vec.x * vec.x + vec.y * vec.y);
}
return total / vecs.length;
}
如果改成 ArrayBuffer,内存布局会更接近 C 语言数组:
function avgLen(vecs: ArrayBuffer): number {
let total = 0;
const float64 = new Float64Array(vecs);
for (let i = 0; i < float64.length; i += 2) {
const x = float64[i];
const y = float64[i + 1];
total += Math.sqrt(x * x + y * y);
}
return total / (float64.length / 2);
}
后一种写法减少了对象访问和内存碎片,更容易命中 CPU 缓存。它说明一个事实:性能差异不只由语言决定,也由内存布局、算法、运行时优化、数据规模和浏览器实现共同决定。
更实际的判断方式是:
| 场景 | 是否适合 Wasm |
|---|---|
| 表单、按钮、页面路由、普通业务交互 | 不适合,JavaScript 更简单 |
| 图片滤镜、视频转码、压缩解压、加密哈希 | 适合,CPU 计算密集 |
| 大型 C/C++ 桌面软件迁移到 Web | 适合,可复用已有代码 |
| 小型业务逻辑函数 | 多数不适合,胶水代码和加载成本可能抵消收益 |
| 需要运行不可信第三方代码 | 适合,Wasm 沙箱有明显价值 |
| 需要频繁访问 DOM | 不适合,Wasm 不能直接操作 DOM,仍要跨 JavaScript 调用 |
如何在浏览器里运行一个 Wasm 模块
浏览器提供了 WebAssembly API,可以直接编译和实例化 Wasm 模块。
这段代码构造了一个极小的 Wasm 二进制模块,并导出 add 和 square 两个函数:
const bytes = new Uint8Array(`
00 61 73 6d 01 00 00 00 01 0c 02 60 02 7f 7f 01
7f 60 01 7f 01 7f 03 03 02 00 01 07 10 02 03 61
64 64 00 00 06 73 71 75 61 72 65 00 01 0a 13 02
08 00 20 00 20 01 6a 0f 0b 08 00 20 00 20 00 6c
0f 0b
`.trim().split(/[\s\r\n]+/g).map(str => parseInt(str, 16)));
WebAssembly.compile(bytes).then(module => {
const instance = new WebAssembly.Instance(module);
const { add, square } = instance.exports;
console.log("2 + 4 =", add(2, 4));
console.log("3^2 =", square(3));
console.log("(2 + 5)^2 =", square(add(2, 5)));
});
真实项目不会这样手写二进制字节。更常见的方式是用 Emscripten 编译 C/C++。
一个简单的 C 文件:
// calc.c
int add(int a, int b) {
return a + b;
}
int square(int x) {
return x * x;
}
使用 Emscripten 编译:
emcc calc.c \
-O3 \
-s WASM=1 \
-s EXPORTED_FUNCTIONS='["_add","_square"]' \
-s EXPORTED_RUNTIME_METHODS='["ccall","cwrap"]' \
-o calc.js
然后在 JavaScript 中调用:
<script src="./calc.js"></script>
<script>
Module.onRuntimeInitialized = () => {
const add = Module.cwrap("add", "number", ["number", "number"]);
const square = Module.cwrap("square", "number", ["number"]);
console.log(add(2, 4));
console.log(square(3));
};
</script>
这类调用通常由胶水代码完成。胶水代码负责加载 .wasm、准备导入函数、管理内存视图、完成 JavaScript 和 Wasm 之间的数据转换。
Web 端适合 Wasm 的应用类型
Wasm 在前端最典型的价值,是把原本难以放进浏览器的计算能力搬进 Web 应用。
| 类型 | 代表场景 | Wasm 起到的作用 |
|---|---|---|
| 3D 地图和可视化 | Google Earth 一类复杂地图 | 复用高性能图形和几何计算逻辑 |
| 在线设计工具 | Figma、Photoshop Web 版 | 核心绘图、图像处理、布局计算用 Wasm 承载,界面仍由 Web 技术组织 |
| 音视频处理 | FFmpeg.wasm、视频上传处理 | 在浏览器内完成转码、抽帧、封面处理、格式解析 |
| 在线会议 | 虚拟背景、降噪、实时字幕 | 使用 SIMD 加速图像和音频处理 |
| 机器学习 | TensorFlow.js Wasm 后端 | 在没有 GPU(图形处理器)后端时,用 Wasm + SIMD + 多线程提升推理速度 |
| 游戏引擎 | Unity WebGL、部分 Unreal HTML5 方案 | 引擎底层和脚本逻辑编译到 Wasm,渲染 API 适配 WebGL 或 WebGPU |
这里需要区分 CPU 和 GPU 的职责。Wasm 主要解决 CPU 侧计算问题;图形渲染、通用 GPU 计算通常依赖 WebGL 或 WebGPU。一个 3D 应用变快,可能是因为渲染管线改进,也可能是因为 CPU 侧逻辑迁移到 Wasm,不能把所有收益都归到 Wasm 上。
音视频、图像处理和机器学习推理经常还会用到 SIMD(Single Instruction Multiple Data,单指令多数据)。SIMD 可以用一条指令处理多份数据,例如一次处理多个像素或多个浮点数。Wasm SIMD 的价值在于提供相对统一的可移植向量指令,让不同 CPU 架构都能获得一定加速。
Wasm 走向服务端:WASI 和轻量沙箱
Wasm 早期名字里带着 “Web”,但它并不只能运行在浏览器中。脱离浏览器后,Wasm 要面对一个新问题:文件、网络、时钟、随机数、环境变量这些系统能力来自哪里?
浏览器里可以通过 JavaScript 间接访问宿主能力;服务端没有浏览器,就需要一套标准系统接口。这就是 WebAssembly System Interface(WASI,Wasm 系统接口)。
flowchart TD
A[同一个 .wasm 模块] --> B[Wasmtime on Linux]
A --> C[Wasmtime on macOS]
A --> D[Wasmtime on Windows]
A --> E[边缘节点 Runtime]
B --> F[WASI: 文件 / 网络 / 时钟 / 随机数]
C --> F
D --> F
E --> F
F --> G[底层操作系统 API]
C 语言的跨平台通常是“源码级跨平台”:同一份源码在不同系统上分别编译,生成不同机器码。
Wasm + WASI 追求的是“二进制级跨平台”:同一个 .wasm 文件交给不同平台上的 Wasm 运行时,由运行时适配底层系统差异。
这和 Java 虚拟机有相似之处,但 Wasm 更强调多语言编译目标、沙箱能力、轻量启动和嵌入式运行时。
Wasmtime 是 WASI 的重要实现
Wasmtime 是 Bytecode Alliance 推动的 Wasm 运行时实现,使用 Rust 编写,支持 WASI,并且面向服务端、嵌入式、插件系统等场景。
服务端使用 Wasm 时,常见执行链路是:
sequenceDiagram
participant Client as 调用方
participant Host as 宿主服务
participant Runtime as Wasm 运行时
participant Module as .wasm 模块
participant OS as 操作系统
Client->>Host: 请求执行某个函数
Host->>Runtime: 创建实例并传入允许的能力
Runtime->>Module: 调用导出函数
Module->>Runtime: 请求 WASI 能力
Runtime->>OS: 在权限范围内访问系统资源
OS-->>Runtime: 返回结果
Runtime-->>Host: 返回模块执行结果
Host-->>Client: 返回响应
这里的关键点是“能力显式授予”。Wasm 模块不能默认读任意文件、连任意网络、访问任意系统调用。宿主服务给什么,它才能用什么。
服务端 Wasm 适合哪些场景
云计算中的隔离运行环境可以粗略分成三层:
| 层级 | 代表技术 | 特点 |
|---|---|---|
| Hypervisor VM / microVM | VMware、Firecracker | 隔离强,成本相对高,启动更重 |
| 应用容器 | Docker、containerd | 生态成熟,适合打包完整应用和依赖 |
| 高级语言虚拟机 / Wasm Runtime | JVM、Python Runtime、Wasmtime | 更轻,适合函数、插件、细粒度任务 |
Wasm 不是 Docker 的完全替代品。Docker 适合交付完整应用环境,Wasm 更适合执行边界清晰、依赖可控、启动要求高、需要沙箱隔离的小模块。
| 场景 | 为什么适合 Wasm |
|---|---|
| Serverless / FaaS | 模块体积小,启动快,适合高密度部署短生命周期函数 |
| BaaS 平台扩展 | 平台可让用户上传逻辑代码,同时用沙箱限制能力边界 |
| 第三方插件系统 | 插件不能直接访问宿主进程和系统资源,安全模型更清晰 |
| 数据库 UDF | User Defined Function(UDF,用户自定义函数)可用多语言编写,并在数据库内安全执行 |
| 边缘计算 | 节点资源有限,Wasm 运行时轻,分发 .wasm 比分发完整容器更省 |
| 可信执行环境 | Trusted Execution Environment(TEE,可信执行环境)中可用 Wasm 承载多语言逻辑 |
数据库 UDF 是一个很典型的例子。传统数据库扩展往往绑定某几种语言,例如 JavaScript、Java 或 Python。若数据库内嵌 Wasm 运行时,就可以允许用户用 Rust、C、Go 等语言写函数,统一编译成 Wasm 后执行。数据库只需要控制 Wasm 运行时的权限、内存、时间和输入输出边界。
什么时候不该用 Wasm
Wasm 很适合做计算内核和沙箱模块,但它也有明显成本。
| 问题 | 影响 |
|---|---|
| 调试复杂 | .wasm 是低级格式,源码映射、栈信息、符号表需要额外配置 |
| 胶水代码成本 | JavaScript 和 Wasm 之间传字符串、对象、数组时可能需要拷贝和编码转换 |
| DOM 访问不直接 | Wasm 不能像 JavaScript 那样直接操作 DOM,频繁跨边界调用会拖慢性能 |
| 包体积可能变大 | 引入运行时、标准库、语言支持层后,小功能未必划算 |
| 生态差异 | 不同语言编译到 Wasm 的成熟度不一样,C/C++/Rust 通常更成熟 |
| 性能不稳定 | 加速幅度依赖算法、数据规模、浏览器、硬件、编译器和运行时 |
选择 Wasm 前,可以按这张清单判断:
flowchart TD
A[准备引入 Wasm] --> B{是否存在明确 CPU 瓶颈?}
B -- 否 --> C[继续使用 JavaScript 或常规后端实现]
B -- 是 --> D{是否能把热点逻辑拆成独立模块?}
D -- 否 --> E[先重构模块边界]
D -- 是 --> F{是否有可复用 C/C++/Rust 等代码?}
F -- 是 --> G[评估编译到 Wasm 的成本]
F -- 否 --> H{重写成本是否低于 JS 优化成本?}
H -- 否 --> I[优先优化 JS 数据结构和算法]
H -- 是 --> G
G --> J[做基准测试]
J --> K{收益是否覆盖加载和维护成本?}
K -- 是 --> L[生产化接入 Wasm]
K -- 否 --> M[保留现有方案]
经验上,真正适合 Wasm 的前端模块通常有三个特征:输入输出边界清晰、计算时间明显、内部不频繁访问 DOM 或大量 JavaScript 对象。
Wasm 3.0 后更值得关注的方向
Wasm 3.0 强化了几个长期方向。
多语言运行时会更自然
GC、强类型引用、异常处理、尾调用等能力,会让更多高级语言不用携带沉重的自定义运行时就能生成 Wasm。过去 C/C++/Rust 更适合 Wasm,是因为它们的内存模型和 Wasm 更贴近;随着 GC 等能力补齐,Java、Kotlin、Scala、C# 等语言会有更好的编译目标。
服务端插件和函数平台会继续增长
Wasm 的沙箱、冷启动、跨语言编译目标,天然适合插件系统和函数平台。相比直接运行用户上传的脚本,Wasm 更容易限制权限;相比容器,Wasm 启动和分发更轻。
确定性执行会打开更多严肃场景
区块链、多人协同、仿真、状态机复制等系统,都要求相同输入在不同机器上得到相同结果。浮点 NaN、向量指令、硬件差异都可能破坏一致性。Wasm 3.0 的确定性执行配置,让这类系统更容易建立可验证的执行环境。
WebGPU 和 Wasm 会组合使用
Wasm 负责 CPU 侧逻辑,WebGPU 负责 GPU 图形渲染和通用计算。浏览器内的大型应用,例如游戏、建模、视频编辑、AI 推理,往往不会只靠 Wasm 提速,而是 Wasm、WebGPU、Web Worker、多线程、SIMD 一起组成性能方案。
结语
WebAssembly 的价值不是“让所有代码都换成 Wasm”,而是给高性能计算、跨语言复用、沙箱执行和跨平台分发提供了一个标准化底座。
在 Web 端,它适合承接音视频、图像、游戏、设计工具、机器学习推理这类 CPU 密集模块;在服务端,它适合做函数运行时、插件系统、数据库 UDF、边缘计算和安全隔离层。Wasm 3.0 增强了内存、类型系统、GC、异常、尾调用、SIMD 和确定性执行,让它从“浏览器里的高性能补丁”继续向“通用可移植执行层”靠近。
真正落地时,不需要把 Wasm 当成银弹。先定位性能瓶颈,再拆分模块边界,用基准测试验证收益,确认加载、调试、胶水代码和维护成本都能接受,再把核心计算迁移过去。这样使用 Wasm,才容易得到稳定收益。