开发者都知道 JavaScript 是单线程的,即只有一个调用栈和堆,所有代码都在浏览器渲染进程的主线程中执行,当然也包括本文的主角,即 WebAssembly。
虽然 WebAssembly 由高级语言编译而成,而且执行速度比 JavaScript 要快得多。然而,如果让 WebAssembly 具备多线程的能力,浏览器引入 WebAssembly 代码后运行效率将得到显著的改善。示例
- Google Earth: 其 Web 版本也使用 WebAssembly 多线程
- FFMPEG.WASM :流行的 FFmpeg 多媒体工具链的 WebAssembly 版本,使用 WebAssembly 线程直接在浏览器中高效地编码视频。
WebAssembly 多线程支持是 WebAssembly 最重要的性能增强之一,其允许开发者在单独的内核上并行运行部分代码,或者在同一个内核中运行具有不同输入的同一份代码,从而最大限度的利用内核资源并减少总体执行时间。
1.WebAssembly 多线程如何工作
WebAssembly 线程并非一个单独的功能,而是多个组件的组合,其允许 WebAssembly 应用程序在 Web 上使用传统的多线程。
1.1 WebAssembly 组件之 Web Worker
浏览器中的多线程目前只能通过 Web Worker 实现。
因此,WebAssembly 的第一个组件是 Worker, WebAssembly 多线程支持需要使用新的 Worker 构造函数来创建新的底层线程。
1 | const myWorker = new Worker("worker.js"); |
每个线程都会加载 JavaScript 胶水,然后主线程使用 Worker.postMessage 方法与其他线程共享编译后的 WebAssembly.Module 以及共享的 WebAssembly.Memory,从而建立通信并允许所有这些线程在同一共享内存上运行相同的 WebAssembly 代码,而无需再次通过 JavaScript。
接下来一起看看 WebAssembly.Module 和 WebAssembly.Memory 的作用。
- WebAssembly.Module
该对象包含已经由浏览器编译的无状态 WebAssembly 代码,可以高效地与 Worker 共享和多次实例化。
以下示例使用
WebAssembly.compileStreaming() 方法编译 simple.wasm 加载后的字节码,并将返回的 WebAssembly.Module 实例通过 postMessage 发送给 worker。
1 | const worker = new Worker("wasm_worker.js"); |
wasm_worker.js 定义了模块需要使用的导入对象,然后创建一个事件处理器,以接受主线程发送的模块。在接收到模块后使用 WebAssembly.instantiate() 方法创建一个实例并调用其导出的函数。
1 | const importObject = { |
- WebAssembly.Memory
WebAssembly.Memory() 构造函数创建一个新的 Memory 对象,该对象的 buffer 属性是一个可调整大小的 ArrayBuffer ,其内存储的是 WebAssembly 实例所访问内存的原始字节码。
从 JavaScript 或 WebAssembly 中所创建的内存,可以由 JavaScript 或 WebAssembly 来访问及更改。
1 | var memory = new WebAssembly.Memory({initial: 10, maximum: 100}); |
Web Worker 已经存在十多年了,受到广泛支持,并且不需要任何特殊标志。
1.2 WebAssembly 组件之 SharedArrayBuffer

WebAssembly 内存由 JavaScript API 中的 WebAssembly.Memory 对象表示。 默认情况下,WebAssembly.Memory 是 ArrayBuffer 的包装器 ,只能由单个线程访问的原始字节缓冲区。
1 | > new WebAssembly.Memory({initial:1, maximum:10}).buffer |
为了支持多线程,WebAssembly.Memory 也获得了一个共享变体。 当通过 JavaScript API 或 WebAssembly 二进制文件本身使用 shared 标志创建时,其会成为 SharedArrayBuffer 的包装器,可以与其他线程共享并从任意一侧同时读取或修改。
1 | > new WebAssembly.Memory({initial:1, maximum:10, shared:true}).buffer |
与通常用于主线程和 Web Workers 之间通信的 postMessage 不同,SharedArrayBuffer 不需要复制数据,甚至不需要等待事件循环(Event Loop)来发送和接收消息。 相反,所有线程几乎都会立即看到任何更改,这使其成为传统同步原语(Synchronisation Primitives)更好的编译目标。
比如以下代码将 SharedArrayBuffer 传递给 worker 操作:
1 | const myWorker = new Worker("worker.js"); |
因为安全问题 Chrome 68 通过利用站点隔离功能在 2018 年才重启 SharedArrayBuffer,该功能可将不同的网站置于不同的进程中,并使使用 Spectre 等旁路攻击变得更加困难。 然而,这种缓解措施仍然仅限于 Chrome 桌面版,因为站点隔离是一项相当昂贵的功能,并且无法默认为低内存移动设备上的所有站点启用,其他供应商也尚未实现。
不过到 2020 年,Chrome 和 Firefox 都实现了站点隔离,以及网站通过 COOP 和 COEP 标头选择加入该功能的标准方法。 即使在低功耗设备上,选择加入机制也允许使用站点隔离,因为在低功耗设备上为所有网站启用站点隔离的成本太高。 要选择加入,请将以下标头添加到服务器配置中的主文档中:
1 | Cross-Origin-Embedder-Policy: require-corp |
通过以上方式就可以访问 SharedArrayBuffer,包括: SharedArrayBuffer 支持的 WebAssembly.Memory、精确计时器、内存测量以及出于安全原因需要隔离源的其他 API。
1.3 WebAssembly 组件之 WebAssembly 原子(Atomics)
虽然 SharedArrayBuffer 允许每个线程读取和写入同一内存,但为了正确通信,开发者需要确保其不会同时执行冲突的操作, 例如:一个线程可能开始从共享地址读取数据,而另一个线程正在写入数据,此类错误称为竞争条件。 为了防止竞争条件,开发者需要以某种方式同步这些访问,这就是原子操作的用武之地。
WebAssembly 原子是 WebAssembly 指令集的扩展,允许 “原子地” 读取和写入小数据单元,通常是 32 位和 64 位整数。 也就是说,确保没有两个线程同时读取或写入同一单元,从而在底层防止此类冲突。
此外,WebAssembly 原子还包含两种指令类型,即 “wait” 和 “notify”,从而允许一个线程在共享内存中的给定地址上休眠(“等待”),直到另一个线程通过 “通知” 将其唤醒。所有更高级别的同步原语,包括:通道、互斥锁和读写锁都建立在这些指令的基础上。
2.WebAssembly 多线程如何工作
WebAssembly 原子和 SharedArrayBuffer 是相对较新的功能,尚未在所有支持 WebAssembly 的浏览器中提供。

为了确保所有用户都可以加载当前应用程序,需要构建两个不同版本的 Wasm 来实现渐进增强,一个版本支持多线程,另一个版本不支持,然后根据功能检测结果加载支持的版本。
要在运行时检测 WebAssembly 是否支持多线程可以使用 wasm-feature-detect 库,其用于检测当前环境支持哪些 WebAssembly 功能。
- ✅ 在浏览器、Node 和 Deno 中运行
- ✅ Tree-shakable(仅打包使用的探测器)
- ✅ 作为 ES6、CommonJS 和 UMD 模块提供。
- ✅ 兼容 CSP
- ✅ 所有检测器加起来仅 ~730B gzipped
wasm-feature-detect 使用起来也非常简单,如下所示:
1 | import {threads} from 'wasm-feature-detect'; |
比如该库还能监测是否支持 SID:
1 | import {simd} from "wasm-feature-detect"; |
3. 推荐 Comlink 创建线程池
Comlink 让 Web Workers 变得非常简单,其是一个小型库(1.1kB),消除了考虑 postMessage 的心理障碍,并隐藏了正在与 Worker 一起工作的事实。
在更抽象的层面上,Comlink 底层是基于 postMessage 和 ES6 代理的 RPC 实现。目前 Comlink 在 Github 通过 Apache-2.0 协议开源,有超过 10.7k 的 star,是一个优质的前端开源项目。
下面是 main.js 的示例:
1 | import * as Comlink from "https://unpkg.com/comlink/dist/esm/comlink.mjs"; |
下面是 worker.js 的代码示例:
1 | importScripts("https://unpkg.com/comlink/dist/umd/comlink.js"); |