Web Worker定义
Web Worker 是浏览器提供的多线程能力,允许在主线程之外运行 JavaScript。
它的目标只有一个: 👉 把耗时任务从 UI 线程中挪走,让页面保持流畅。
Web Worker作用
解决主线程的现实困境,主线程同时负责:
- JS 执行
- 页面渲染
- 用户交互
- 事件响应
一旦遇到👇
- 大量计算
- JSON 解析
- 文件处理
- 图片 / 视频处理
- 高频轮询 / 长时间任务
👉 UI 就会卡,页面就会“假死”。
工作原理
1. 线程模型
主线程(UI / React)
│
├── postMessage
▼
Worker 线程(独立事件循环)
│
└── postMessage
- Worker 运行在 独立线程
- 拥有自己的事件循环
- 不会阻塞 UI
2. 通信机制
Worker 与主线程的通信是 完全异步 的。
- 异步消息传递
- Worker 和主线程通过
postMessage发送消息。 - 消息不会立即返回结果,而是放入消息队列,等待目标线程处理。
- 默认采用 structured clone(深拷贝)
- 可使用 Transferable(如 ArrayBuffer)转移所有权,避免拷贝
- 接收方通过
onmessage回调处理消息。
// main.js
const worker = new Worker('worker.js');
worker.onmessage = (e) => {
console.log('收到 Worker 消息:', e.data);
};
worker.postMessage('Hello Worker'); // 异步发送
console.log('这行会先打印');
输出顺序:
这行会先打印
收到 Worker 消息: HELLO WORKER
可以看到 postMessage 不会阻塞主线程。
- 异步通信原因
- 异步通信避免阻塞主线程 UI 渲染。
- Worker 和主线程运行在不同线程,直接同步调用会涉及复杂锁和线程安全问题。
- 异步模型符合 JS 单线程事件循环机制,安全且高效。
- 注意
- 虽然通信异步,但消息发送是可靠的,消息按顺序到达。
- 如果需要“同步”效果,可以在主线程用
Promise包装 Worker 消息:
function workerTask(data) {
return new Promise(resolve => {
worker.onmessage = e => resolve(e.data);
worker.postMessage(data);
});
}
Worker 与主线程通信是异步的,通过消息队列和事件循环实现安全高效的数据交换。
3. 不能访问DOM
Worker 不访问 DOM 是浏览器为了线程安全、性能优化而设计的约束。它专注“计算”,主线程专注“渲染”。
- Web Worker 的本质
- Web Worker 是浏览器提供的多线程环境,让 JS 能够在主线程之外运行。
- 它有自己的全局上下文(
WorkerGlobalScope),并不是window,所以不包含 DOM API。 - 目的是为了避免阻塞主线程,尤其是渲染线程。浏览器 UI 渲染和 DOM 操作都在主线程。
- DOM 不是线程安全的
- DOM 树是共享资源,如果多个线程直接操作,会产生竞态条件和不确定行为。
- 比如:一个 Worker 修改了 DOM 节点的属性,主线程同时在渲染这个节点,会出现不可预期的渲染结果或浏览器崩溃。
简单理解:DOM 就像一个昂贵的共享数据库,不能被多个线程同时写入,否则数据容易“炸”。
- Worker 与主线程的通信机制
- Worker 和主线程通过
postMessage进行通信,消息会被序列化(structured clone) 。 - Worker 可以处理数据、计算、加密等“纯逻辑”,然后把结果发送给主线程,由主线程更新 DOM。
// main.js
const worker = new Worker('worker.js');
worker.onmessage = (e) => {
document.getElementById('output').textContent = e.data;
};
worker.postMessage('Hello Worker');
// worker.js
onmessage = (e) => {
const result = e.data.toUpperCase(); // 模拟耗时计算
postMessage(result);
};
- 这样既保证了 DOM 操作在主线程安全进行,又利用 Worker 提升计算性能。
- SharedWorker、Service Worker 等都是类似的设计:它们也不能直接操作 DOM。
- 如果真的想“间接操作”,Worker 可以返回数据,主线程再去渲染。
┌───────────────┐
│ 主线程 UI │
│ ┌───────────┐│
│ │ DOM ││ ← 只能主线程操作
│ └───────────┘│
└─────┬────────┘
│ postMessage / onmessage
│
▼
┌───────────────┐
│ Web Worker │ ← 只能做计算、数据处理
│ │
│ 复杂逻辑 / AI │
│ 数据处理 / 加密│
└───────────────┘
▲
│ postMessage / onmessage
│
┌─────┴────────┐
│ 主线程接收数据│
│ 更新 DOM │
└──────────────┘
Web Worker分类
| 类型 | 名称 | 特点 | 使用场景 |
|---|---|---|---|
| Dedicated Worker | 专用 Worker | 一对一、最常用 | 计算、解析、后台任务 |
| Shared Worker | 共享 Worker | 多页面共享 | 多标签页共享状态 |
| Service Worker | service Worker | 拦截网络请求 | PWA / 离线缓存 |
| Worklet | —— | 低延迟 | 音频 / 渲染 |
👉 日常业务中 90% 用 Dedicated Worker
Shared Worker 的作用就是 同源下多窗口或多个脚本共享同一个 Worker,适合一些需要跨标签页共享状态或逻辑的场景。
| 场景 | 原因 / 优势 |
|---|---|
| 跨标签页共享 WebSocket | 比如你网站在多个标签页同时打开,要共享同一个 WebSocket 连接,避免每个标签页都建立一个。 |
| 全局状态管理 | 多标签页共享登录状态、实时消息状态、计数器等。 |
| 缓存数据 | 对一些计算结果或请求结果进行缓存,所有标签页可直接复用,不必重复计算或请求。 |
| 后台任务 | 跨标签页执行定时任务、轮询接口、同步数据等。 |
// shared-worker.js
onconnect = (e) => {
const port = e.ports[0];
port.onmessage = (msg) => {
port.postMessage(`收到: ${msg.data}`);
};
};
// main.js
const worker = new SharedWorker('shared-worker.js');
worker.port.start(); // 必须启动 port
worker.port.postMessage('Hello SharedWorker');
worker.port.onmessage = (e) => console.log(e.data);
当你需要多个同源上下文共享同一份逻辑、状态或连接时,选择
Shared Worker。
如果只在一个页面里使用,普通 Worker 就够了。
Web Worker特点
- ✅ 真正的并行执行
- ✅ 避免主线程阻塞
- ❌ 不能操作 DOM
- ❌ 创建和通信有成本
- ⚠️ 数据传输需要考虑性能
- ✅ 优点
- 提升页面响应速度
- 适合 CPU 密集型任务
- 可作为后台“计算引擎”
- 可发送异步请求
- 可运行 fetch / WebSocket / IndexedDB
- ❌ 缺点
- 无法操作 DOM
- 通信存在数据拷贝成本
- 创建/销毁开销不小
- 调试、打包需要额外配置
适用场景
- 🔥 强烈推荐使用
- 大数据计算 / 排序 / 过滤
- 图片处理、压缩、滤镜
- 视频转码、音频分析
- 文件解析(CSV / Excel / JSON)
- 加密、解密、哈希计算
- ⚠️ 谨慎使用
- 轻量逻辑
- 高频短任务
- 强依赖 DOM 的操作
前端使用
- ❌ 常见误区
- 每个组件 new 一个 Worker
- Worker 生命周期不清理
- 主线程/Worker 强耦合
- ✅ 推荐模式:Hook + 单例 Worker
const { post } = useWorker() // React Hook 库
const result = await post('compute', payload)
特点:
- Worker 只创建一次
- Promise 化调用
- 组件只关心结果
Web Worker 是全局资源,而不是组件资源。
内存泄漏
Worker 使用不当确实容易造成 内存泄漏,尤其是长期运行的 Worker。
为什么会内存泄漏
-
Worker 占用独立线程内存
- Worker 有自己的全局作用域(
WorkerGlobalScope),所有变量和闭包都在这个线程中存在。 - 如果 Worker 长时间不结束,它占用的内存不会被主线程回收。
- Worker 有自己的全局作用域(
-
未解除事件监听
- 主线程给 Worker 注册了
onmessage或addEventListener,但 Worker 已不再需要或标签页关闭,引用仍存在。
- 主线程给 Worker 注册了
-
引用外部资源
- Worker 持有大对象或数据(比如缓存、ArrayBuffer、图片等),不会被 GC 回收,导致内存增长。
正确终止 Worker
1:主线程主动终止
const worker = new Worker('worker.js');
// 使用完毕后
worker.terminate(); // 立即停止 Worker 线程
terminate()是 同步操作,立即停止 Worker。- Worker 内部不会再执行任何代码,也不会触发
onclose或finally,要提前处理清理逻辑。
2:Worker 自行关闭
// worker.js
self.close(); // Worker 自己可以调用关闭自己
- 功能等同于
terminate(),在 Worker 内部调用。 - 可在完成任务或遇到异常时使用。
3:解除事件监听
- 主线程应移除绑定的事件,避免保留引用:
worker.onmessage = null;
worker.onerror = null;
- 对使用
addEventListener的,记得removeEventListener。
使用建议:
- 短生命周期:尽量不要让 Worker 长时间挂起,任务完成立即 terminate。
- 数据传输:用
Transferable Objects(如ArrayBuffer)避免复制大数据,提高性能同时减少内存占用。 - SharedWorker:如果是跨标签页共享,要确保 最后一个标签页关闭后释放 Worker,避免长期占用。
Worker 占用独立线程内存,如果不 terminate 或解除事件引用,就容易泄漏。使用完毕及时
worker.terminate()或在 Worker 内部self.close(),并解除事件监听,是保证内存安全的关键。
Web Worker + WebSocket
一个非常实用的组合:
- Worker 里维护 WebSocket
- 处理心跳 / 重连 / 解析
- 主线程只负责 UI 渲染
优势:
- UI 不受网络抖动影响
- 连接逻辑高度解耦
- 更稳定的实时通信
┌────────────┐
│ React UI │
│ │
│ 订阅数据 │
│ postMessage│
└─────▲──────┘
│
│ structured clone
▼
┌────────────┐
│ Web Worker │
│ │
│ WebSocket │
│ 心跳/重连 │
│ 限流/聚合 │
└────────────┘