Skip to content
Web Worker 多线程机制
概述
Web Worker 在独立线程中运行一份隔离的 JavaScript 执行上下文。主线程与 Worker 之间没有共享内存(SharedArrayBuffer 除外),所有通信依赖消息传递。Worker 的基线成本是消息的序列化与反序列化——如果通信开销超过计算收益,Worker 不会让程序变快。
基本概念
Worker 类型
DedicatedWorker(专用 Worker)
一个new Worker()创建一个独立实例,仅创建它的脚本可以与之通信。SharedWorker
同一源下、相同 URL 的 SharedWorker 会复用已有的实例。多个页面、iframe 或 Worker 可通过port与之通信。ServiceWorker
作为网络代理层运行,生命周期由浏览器管理,不属于通用的多线程工具。本书不展开。
工作原理
消息传递:结构化克隆与可转移对象
postMessage() 使用结构化克隆(structured clone)算法传递数据——序列化的是深拷贝,不是引用。支持的类型包括基本类型、Object、Array、Date、RegExp、Map、Set、ArrayBuffer、TypedArray、Blob、File、ImageData 等。函数、DOM 节点、Symbol 与 Error 对象不在规范之中(部分浏览器可能支持,但依赖它们会引入不确定性)。
每一条 postMessage 都会完整拷贝数据。发送 10 MB 的 ArrayBuffer 时,结构化克隆的耗时通常在 5–15 ms(取决于设备与数据复杂度)。频繁传递大块数据会让序列化开销吃掉多线程的收益。
Transferable objects:转移所有权
对于 ArrayBuffer,可以选择转移所有权以绕过拷贝:
js
// 主线程
const buffer = new ArrayBuffer(1024 * 1024 * 100); // 100 MB
worker.postMessage({ buffer }, [buffer]);
// 此后主线程中 buffer.byteLength === 0
// 所有权已转移到 Worker 端转移的时间复杂度接近 O(1),与数据大小无关。适合“一方产出数据、另一方处理后返回”的模式。
MessageChannel
MessageChannel 创建一对互相连接的 port,可以在任意两个上下文之间建立额外的直接通道:
js
const channel = new MessageChannel();
worker.postMessage({ port: channel.port2 }, [channel.port2]);
channel.port1.onmessage = (e) => {
// 来自 Worker 的回传消息
};适合“请求-响应”模式:主线程发任务,Worker 通过专用 port 返回结果,不干扰 Worker 的主消息通道。
Worker 线程的事件循环
Worker 拥有独立的事件循环,流程如下:
- 从 Worker 的 task queue 取出任务
- 执行任务(含
onmessage回调) - 清空 microtask queue
- 循环(没有 Update the rendering 阶段)
Worker 没有 DOM、没有渲染管线,因此 requestAnimationFrame 与 requestIdleCallback 不可用。Promise、async/await、setTimeout/setInterval 的行为与主线程一致(例如嵌套 5 层后 setTimeout 的最小延迟仍约为 4 ms)。
主线程与 Worker 的消息时序
text
主线程 Worker 线程
─────────────────────────────────────────
执行同步代码
postMessage(data) ──────────→ 入队 Worker task queue
继续主线程代码 │
microtask 清空 │
可能的渲染 ↓
执行 onmessage 回调
self.postMessage(result)
│
继续 Worker 代码…
↓
←────── 入队主线程 task queue
│
主线程收到 onmessageworker.postMessage() 不会立刻触发 Worker 端的 onmessage——消息进入 Worker task queue,排在前面的任务之后。Worker 发回的消息同理,进入主线程 task queue。
浏览器进程模型
在 Chrome 中,每个 Worker 运行在独立线程中,但它们共享同一个渲染进程:
text
渲染进程(Renderer Process)
├── 主线程(Main Thread)
│ ├── JavaScript 执行
│ ├── 样式计算 / 布局 / 绘制
│ └── DOM / 事件处理
├── Worker 线程 1
│ └── JavaScript 执行(无 DOM)
├── Worker 线程 2
│ └── JavaScript 执行(无 DOM)
├── Compositor 线程
└── IO 线程每个 Worker 对应一个独立的 V8 isolate,拥有独立的堆内存。Worker 的垃圾回收(GC)不会阻塞主线程的 JS 执行——除非使用了 SharedArrayBuffer 且需要跨线程同步。
创建 Worker 线程的底层路径(Chromium 源码简示):
WorkerMessagingProxy在渲染进程中创建WorkerThread对象WorkerThread::Start()调用base::Thread::Start()- 最终通过
pthread_create(Linux/macOS)或CreateThread(Windows)创建操作系统线程 - 新线程中初始化 V8 isolate 并执行 Worker 脚本
基本用法
专用 Worker
js
// 主线程
const worker = new Worker('./worker.js');
worker.postMessage({ type: 'compute', data: largeArray });
worker.onmessage = (e) => {
console.log('result:', e.data);
};
worker.onerror = (e) => {
console.error('worker error:', e.message, 'at line', e.lineno);
};
// worker.js
self.onmessage = (e) => {
const result = heavyCompute(e.data);
self.postMessage(result);
};worker.postMessage() 发往 Worker,self.postMessage() 发回主线程,两者是独立的单向通道。
共享 Worker
js
// 主线程
const worker = new SharedWorker('./shared-worker.js');
worker.port.postMessage(data);
worker.port.onmessage = (e) => { /* ... */ };
// shared-worker.js
self.onconnect = (e) => {
const port = e.ports[0];
port.onmessage = (event) => {
port.postMessage(handleData(event.data));
};
};同源、同 URL 的 SharedWorker 只会创建一次实例。每次 new SharedWorker 都会触发共享 Worker 的 onconnect 事件,但不会重复创建 worker 实例。
终止 Worker
worker.terminate():主线程强制终止,Worker 代码立即停止,不等待当前任务结束。self.close():Worker 内部主动终止,当前任务执行结束后不再处理后续任务。
终止后,Worker 实例不可复用,需要 new Worker() 重新创建。
Worker 池
重复创建和销毁 Worker 会产生可观的启动开销(新线程 + V8 isolate + 脚本加载,总耗时约 20–100 ms)。保持存活更高效:
js
class WorkerPool {
constructor(scriptUrl, size = navigator.hardwareConcurrency - 1) {
this.workers = Array.from({ length: size }, (_, i) => ({
worker: new Worker(scriptUrl),
busy: false,
id: i,
}));
}
async run(data, transfer = []) {
const worker = this.workers.find(w => !w.busy);
if (!worker) {
return new Promise(resolve => {
const check = setInterval(() => {
const free = this.workers.find(w => !w.busy);
if (free) {
clearInterval(check);
resolve(this._execute(free, data, transfer));
}
}, 10);
});
}
return this._execute(worker, data, transfer);
}
_execute(worker, data, transfer) {
worker.busy = true;
return new Promise((resolve, reject) => {
worker.worker.onmessage = (e) => {
worker.busy = false;
resolve(e.data);
};
worker.worker.onerror = (e) => {
worker.busy = false;
reject(e);
};
worker.worker.postMessage(data, transfer);
});
}
terminate() {
this.workers.forEach(w => w.worker.terminate());
}
}API
| 接口 | 说明 |
|---|---|
new Worker(url, options?) | 创建专用 Worker,可选 { type: 'module' } 以支持 ES module。 |
new SharedWorker(url, options?) | 创建共享 Worker。 |
worker.postMessage(data, transfer?) | 向 Worker 发送消息,可附带 Transferable 对象列表。 |
worker.terminate() | 强制终止 Worker。 |
worker.onmessage / worker.onerror | 接收来自 Worker 的消息与错误事件。 |
self.postMessage(data, transfer?) | Worker 内部向主线程发送消息。 |
self.close() | Worker 内部关闭自身。 |
self.onmessage / self.onerror | Worker 内部接收消息与错误事件。 |
self.importScripts(...urls) | Worker 内部同步加载并执行外部脚本(阻塞线程)。 |
new MessageChannel() | 创建一对 port,用于任意上下文之间的直接通信。 |
示例
Transferable 零拷贝
js
// 主线程
const buffer = new ArrayBuffer(100 * 1024 * 1024); // 100 MB
worker.postMessage({ buf: buffer }, [buffer]);
// Worker 端
self.onmessage = (e) => {
const buf = e.data.buf; // 所有权已转移
const view = new Float64Array(buf);
// ... 对 view 进行数值计算
self.postMessage({ result: sum }, [buf]); // 可再转回主线程
};主线程在 postMessage 后不再持有数据,避免了序列化 100 MB 的开销。
ES module Worker
js
const worker = new Worker('./worker.js', { type: 'module' });
// worker.js
import { heavyLib } from './lib.js';import 为异步,不阻塞 Worker 线程。Chrome 80+ 支持。
注意点
线程数约束
Worker 数量没有硬性上限,但操作系统支持的并行线程数有限。为减少线程切换开销,Worker 数量一般控制在 navigator.hardwareConcurrency - 1 以内,为主线程保留一个核心。
通信成本与盈亏平衡
总耗时构成为:消息序列化 + Worker 计算 + 消息反序列化 + 事件循环调度延迟。当这些加起来超过主线程直接计算的耗时,使用 Worker 不会带来性能提升,反而增加开销。
实际数据(Chrome 132,8 核 M1 Pro):
| 任务 | 主线程直接 | Worker 执行 | 差异 |
|---|---|---|---|
1M 次 Math.sqrt | ~2 ms | ~8 ms | 更慢 |
10M 次 Math.sqrt | ~18 ms | ~10 ms | 快 1.8x |
| 字符串哈希(1 MB 文本) | ~35 ms | ~20 ms | 快 1.75x |
| JSON 解析(5 MB) | ~45 ms | ~28 ms | 快 1.6x |
| 图像卷积(1024×1024) | ~120 ms | ~25 ms | 快 4.8x |
当计算耗时低于约 5–10 ms 时,通信开销会抵消多线程收益。
错误处理
Worker 内部的未捕获异常触发主线程的 worker.onerror 事件,不会上升到 window.onerror 或 unhandledrejection。脚本加载失败(404 等)同样通过 onerror 报出,new Worker() 本身不抛出异常(网络请求是异步的)。
js
const worker = new Worker('./non-existent.js');
worker.onerror = (e) => {
// e.message, e.filename, e.lineno, e.colno
};importScripts 是同步阻塞
importScripts('./lib.js') 会同步阻塞 Worker 线程直到脚本加载并执行完毕。需要异步加载时,改用 ES module Worker。
SharedArrayBuffer 与 Atomics
SharedArrayBuffer 允许多个 Worker 与主线程共享同一块内存,结合 Atomics 可实现等待 / 通知等同步操作。但必须手动管理竞态,且站点需要设置 COOP 与 COEP 头部,引入成本较高。
WebSocket 与 Worker
WebSocket 的数据接收和处理本质是异步的(不会阻塞渲染),仅在接收的数据需要大量解析(如每 5 秒到达 10 MB 的 JSON)时,移到 Worker 才有实际收益。
移动端
低端 Android 设备通常仅有 2–4 个小核,创建额外线程的收益远低于桌面端。将长任务拆分为多个小块、配合 requestIdleCallback 分批执行,往往更实用。
限制
- Worker 中无法访问 DOM、
localStorage、sessionStorage。 - 不支持
requestAnimationFrame、requestIdleCallback。 - 结构化克隆不支持函数、DOM 节点、Symbol(规范内),传递 Error 对象的行为因浏览器而异。
- 过多的 Worker 会导致线程切换开销增大,不会线性提升性能。
应用
适合 Worker 的场景
- CPU 密集型纯计算:图像 / 视频处理,加解密,数据压缩,大数组数值计算,大量文本的正则匹配。
- 不依赖 DOM 的解析任务:解析大 CSV / JSON 文件,语法高亮,Markdown 编译。
- WebSocket 数据处理(数据量较大时):数据解析放在 Worker,避免主线程阻塞。
不适合 Worker 的场景
- 计算结果需要立即更新 DOM(通信延迟与序列化成本使直接主线程计算更优)。
- 小规模计算(几百次循环以内),启动与通信开销大于计算本身。
- 需要访问 DOM 或存储的 API。
与其他方案的对比
| 方案 | 执行位置 | 独立事件循环 | DOM 访问 | 适用场景 |
|---|---|---|---|---|
| DedicatedWorker | 独立线程 | 是 | 否 | CPU 密集型计算 |
| SharedWorker | 独立线程 | 是 | 否 | 多页面共享状态 / 连接 |
setTimeout 分片 | 主线程 | 共用 | 是 | 可拆分的轻量长任务 |
requestIdleCallback | 主线程 | 共用 | 是 | 非关键低优先级任务 |
| WASM + Worker | Worker 线程 | 是 | 否 | 数值密集(音视频、游戏) |
| Web Worker + Atomics | 独立线程 | 是 | 否 | 需要共享状态的高频计算 |
setTimeout(fn, 0) 分片不减少总计算时间,只是让浏览器有机会在任务之间执行渲染。Worker 真正利用多核并行,可以缩短 wall-clock time。
参考链接
- HTML Standard §10.2: Dedicated workers and the Worker interface
- Chromium 源码
third_party/blink/renderer/core/workers/:Worker 创建、生命周期及消息传递的浏览器侧实现 - Chromium
base::Thread:底层线程创建路径 - V8
Isolate模型:Worker 与主线程的堆独立性
