Web Worker 是什么?它能解决什么性能问题?
简化版
Web Worker 可以在浏览器后台线程中运行 JavaScript,适合把 CPU 密集计算从主线程移走,避免阻塞页面交互和渲染。Worker 不能直接操作 DOM,主线程和 Worker 通过 postMessage 通信,可配合 Transferable 减少大数据复制成本。
详细版
主线程负责 JS、样式、布局、绘制和用户交互,如果执行大计算,就会造成页面卡顿。Web Worker 提供后台线程能力:
const worker = new Worker('/worker.js')
worker.postMessage({ numbers })
worker.onmessage = (e) => {
console.log(e.data)
}
Worker 中:
self.onmessage = (e) => {
const result = heavyCompute(e.data.numbers)
self.postMessage(result)
}
适合场景包括大数据计算、图片处理、加解密、复杂解析、离线数据预处理。不适合频繁小任务,也不能用来直接更新 DOM。
完整版教学
一、Web Worker 解决的是主线程被计算占住
浏览器页面的主线程非常忙:执行 JS、处理事件、计算样式、布局、绘制调度都可能依赖它。如果一段 JS 连续计算 300ms,用户点击、滚动和渲染都会被延迟。
button.onclick = () => {
const result = heavyCompute(bigData) // 500ms
render(result)
}
在这 500ms 内,页面看起来像“假死”。Web Worker 的价值就是把这类 CPU 密集计算搬到后台线程,让主线程继续响应用户和渲染。
主线程:事件、DOM、渲染调度
Worker:纯计算、解析、数据处理
二、Worker 和主线程通过消息通信
Worker 有独立的全局上下文,不能直接访问主线程变量,也不能直接操作 DOM。双方通过 postMessage 和 message 事件通信。
主线程:
const worker = new Worker('/sum.worker.js')
worker.postMessage({ list: [1, 2, 3] })
worker.onmessage = (event) => {
console.log('result', event.data)
}
Worker:
self.onmessage = (event) => {
const sum = event.data.list.reduce((a, b) => a + b, 0)
self.postMessage(sum)
}
这种通信边界让 Worker 更安全,也避免多个线程同时乱改 DOM。但它也带来序列化和通信成本,所以任务太小不一定值得放进 Worker。
三、Worker 不能直接操作 DOM
Worker 没有 document,不能直接查询节点、修改样式或绑定 DOM 事件。它适合产出数据,最终 DOM 更新仍要回到主线程。
Worker 计算结果
-> postMessage 给主线程
-> 主线程 setState / 更新 DOM / 绘制 Canvas
例如 Worker 解析 10MB JSON 或计算图表点位,解析和计算在 Worker 完成,主线程只拿最终结果渲染图表。这样用户滚动页面时不会被解析任务卡住。
如果把 Worker 当成“后台 DOM 操作线程”,就是典型误解。浏览器的 DOM 仍由页面主线程统一管理。
四、数据复制和 Transferable 的成本
postMessage 默认会使用结构化克隆传递数据。普通对象会被复制,数据很大时成本明显。对于 ArrayBuffer 等对象,可以使用 Transferable,把所有权转移给另一方,减少复制。
const buffer = new ArrayBuffer(1024 * 1024 * 20) // 20MB
worker.postMessage(buffer, [buffer])
转移后,原线程里的 buffer 会变成不可用,因为所有权已经交给 Worker。这样能避免 20MB 数据复制一份,降低内存和时间成本。
| 传递方式 | 特点 | 适合场景 |
|---|---|---|
| 结构化克隆 | 简单但可能复制 | 小对象、普通数据 |
| Transferable | 转移所有权,少复制 | 大 ArrayBuffer |
| SharedArrayBuffer | 共享内存,需要安全条件 | 高性能协作 |
数字例子:如果每秒向 Worker 发送 10 次 20MB 数据,复制会造成巨大内存带宽压力;Transferable 能明显降低这类开销。
五、Worker 适合哪些真实场景
Worker 适合 CPU 密集、可与 DOM 分离、输入输出边界清楚的任务。前端面试常见例子包括图片压缩、Excel 解析、大 JSON 处理、搜索索引、地图轨迹计算、加解密和 WASM 计算。
适合:
大文件解析、图片处理、复杂计算、离线索引
不适合:
简单点击逻辑、小对象格式化、频繁 DOM 读写
假设主线程解析一个 30MB Excel 要 2 秒,用户会明显感到卡顿。放到 Worker 后,解析仍然耗时 2 秒,但主线程可以继续展示进度条、响应取消按钮和处理滚动。
记忆钩子:Worker 的本质收益不是让计算本身神奇变少,而是让计算不要占住交互线程。
六、Worker 的生命周期和工程封装
Worker 创建也有成本,长期不用应终止。可使用 worker.terminate() 从主线程结束 Worker,Worker 内部也可以调用 self.close()。
const worker = new Worker('/worker.js')
// 页面卸载或任务结束
worker.terminate()
工程里常把 Worker 封装成任务池。比如最多创建 4 个 Worker,任务排队执行,避免一次创建 100 个 Worker 把 CPU 和内存打爆。
任务队列 -> Worker Pool(4) -> 结果回主线程
还要处理错误、超时和取消。Worker 报错不应静默失败,主线程要监听 error 和 messageerror,并给用户可理解的反馈。
七、常见误区与追问
- 误区:Worker 可以直接操作 DOM。 Worker 没有 DOM 访问能力,DOM 更新必须回主线程。
- 误区:用了 Worker 计算就会变快。 计算总量不变,收益主要是避免阻塞主线程;通信也有成本。
- 误区:所有任务都适合放 Worker。 小任务和频繁通信任务可能得不偿失。
- 追问:postMessage 传大数据怎么优化? 使用 Transferable 转移
ArrayBuffer,或在安全条件下使用SharedArrayBuffer。 - 追问:Worker 如何取消? 主线程可
terminate(),任务层也可设计取消标记和超时控制。 - 追问:Worker 和 Service Worker 区别? Web Worker 偏计算线程,Service Worker 偏网络代理、缓存和离线能力。
八、加强记忆
Web Worker 记成“后台计算线程”:它能跑 JS 计算,不能碰 DOM;和主线程用消息通信;大数据传输看 Transferable;适合重计算,不适合小碎活。回答时把“不卡 UI”和“计算不一定更少”同时说清楚。