Web Worker:线程模型、类型与通信

面试回答: Web Worker 是浏览器提供的后台线程能力,适合把排序、加密、图像处理等 CPU 密集型任务移出渲染主线程。Worker 不能直接操作 DOM,主线程与 Worker 通过 postMessage 通信,数据默认经过结构化克隆;传输大型 ArrayBuffer 时可以使用 Transferable 转移所有权以减少复制开销。Dedicated Worker 服务单个页面,Shared Worker 可被多个同源页面共享,Service Worker 则是带独立生命周期的网络代理,主要用于离线缓存和请求拦截,不能把三者当成同一种用途。

1. Worker 解决什么问题?

页面的 JavaScript、用户事件和大部分渲染工作共享主线程。一个持续数百毫秒的计算任务会占住调用栈,导致点击无法响应、动画掉帧和页面卡顿。

const result = hugeArray.sort(compare);

事件循环只能安排任务的先后顺序,不能让正在执行的长任务自动并行。Web Worker 提供独立线程,可以把纯计算迁移到后台,让主线程继续响应用户和渲染页面。

Worker 并不会让单个算法天然变快。它的主要价值是隔离主线程阻塞;如果任务很小,线程启动、数据复制和消息通信的成本反而可能更高。

2. 三类 Worker 的职责

类型使用范围主要用途生命周期
Dedicated Worker创建它的页面或脚本单页面 CPU 密集型计算通常随所属页面结束,应用也应主动终止
Shared Worker多个同源页面、标签页或 iframe跨页面共享后台状态或复用连接只要仍有页面连接,就可能继续存活
Service Worker受其作用域控制的页面请求拦截、离线缓存、推送和 PWA由浏览器按事件唤醒和终止,不依附某个页面

日常所说的 new Worker() 指的是 Dedicated Worker

Dedicated Worker

每次创建都会产生一个专用 Worker 实例,适合大数组处理、文件哈希、图像像素计算、压缩和加解密等任务。

Shared Worker

多个同源页面可以连接到同一个 Shared Worker,并通过各自的 MessagePort 通信。它适合跨标签页共享状态或复用一个后台连接,但兼容性、调试和连接生命周期都比 Dedicated Worker 更复杂。

Service Worker

Service Worker 位于页面与网络之间,能够监听 fetch 事件并决定从网络还是缓存返回响应。它不是常驻的计算线程,也不应该用来执行任意长时间 CPU 任务。

3. Worker 中能做什么?

Worker 运行在独立的 WorkerGlobalScope 中,不是 window

可以使用的常见能力包括:

  • fetchXMLHttpRequest 等网络请求;
  • setTimeoutsetInterval
  • URLBlobArrayBuffer 等数据处理 API;
  • IndexedDB;
  • WebAssembly
  • OffscreenCanvas 等适合后台处理的 API。

主要限制包括:

  • 不能直接访问 windowdocument 和页面 DOM;
  • 不能直接读取主线程中的变量;
  • 不能使用 localStorage
  • 页面 UI 必须由主线程更新。

如果后台计算需要影响页面,应把结果发送给主线程,再由主线程修改 DOM。

4. Dedicated Worker 基本用法

主线程创建 Worker,并通过消息传递任务:

const worker = new Worker('./worker.js', { type: 'module' });

worker.postMessage({
  type: 'double',
  payload: [1, 2, 3]
});

worker.addEventListener('message', (event) => {
  console.log(event.data);
  worker.terminate();
});

worker.addEventListener('error', (event) => {
  console.error(event.message);
  worker.terminate();
});

Worker 脚本接收任务并返回结果:

self.addEventListener('message', (event) => {
  if (event.data.type !== 'double') return;

  const result = event.data.payload.map((value) => value * 2);
  self.postMessage(result);
});

实际项目中还需要为消息设计任务 ID、成功结果和错误结果,否则多个并发任务很难正确匹配响应。

self.postMessage({
  id: event.data.id,
  ok: true,
  result
});

5. 数据是共享还是复制?

结构化克隆

postMessage 默认使用结构化克隆算法传递数据。接收方拿到的是独立副本,而不是可以被两个线程同时修改的普通对象引用。

结构化克隆支持对象、数组、Map、Set、Blob、ArrayBuffer 等类型,但函数和 DOM 节点不能直接传递。

Transferable

大型二进制数据如果直接克隆,可能产生明显的复制成本。对于 ArrayBufferMessagePortImageBitmap 等可转移对象,可以把底层资源的所有权直接转交给 Worker:

const buffer = new ArrayBuffer(100 * 1024 * 1024);

worker.postMessage(
  { type: 'process', buffer },
  [buffer]
);

转移完成后,原线程中的 buffer 会被分离,不能继续读写。这不是两个线程共享同一份可变数据,而是一次所有权移交。

SharedArrayBuffer

SharedArrayBuffer 才允许多个线程访问同一块内存,通常需要配合 Atomics 做同步,并要求页面满足跨源隔离等安全条件。除非确实需要高性能共享内存,否则优先使用消息传递,模型更简单也更安全。

6. 生命周期与资源清理

Worker 不再使用时应主动终止:

worker.terminate();

Worker 内部也可以调用:

self.close();

在单页应用中,如果每次进入页面都创建 Worker,却没有在离开页面时终止,后台线程及其持有的数据可能不断累积。创建 Worker 的模块或组件应该同时负责:

  • 移除消息和错误监听器;
  • 取消尚未完成的业务任务;
  • 调用 terminate()
  • 清理为 Worker 创建的临时 Blob URL 等资源。

7. 什么时候应该使用 Worker?

适合:

  • 大规模数组计算、数据聚合和排序;
  • 文件哈希、压缩、解压和加解密;
  • 图片像素、音视频数据预处理;
  • 复杂规则计算或 WebAssembly 任务。

不适合:

  • 需要频繁直接操作 DOM 的逻辑;
  • 本身由浏览器异步处理的普通网络等待;
  • 计算量很小但需要频繁传输大量对象的任务;
  • 强依赖主线程状态、难以划分输入输出边界的代码。

判断标准不是“代码是否异步”,而是它是否长时间占用主线程,以及任务输入输出是否适合通过消息传递。

8. 高频追问

Worker 能解决 JavaScript 单线程问题吗?

它能提供并行计算线程,但不会改变页面主线程仍然单线程执行 JavaScript 和更新 DOM 的事实。多个线程通过消息而不是共享普通对象协作。

Worker 为什么不能操作 DOM?

如果多个线程可以同时读写页面结构,就需要复杂的锁和一致性机制。浏览器把 DOM 修改集中在主线程,换取更清晰的执行与渲染模型。

Worker 越多性能越好吗?

不是。线程会消耗内存和调度资源,任务拆分与通信也有成本。Worker 数量应根据任务特征和设备并发能力控制,不能无限创建。

Worker 脚本可以跨域加载吗?

Worker 脚本加载受来源、CORS、内容安全策略和 Worker 类型影响。工程中通常由构建工具产出同源 Worker 资源,不应把“配置一个 CORS 响应头”当成适用于所有 Worker 的通用结论。

9. 总结

  • Web Worker 的核心价值是把 CPU 密集型任务移出渲染主线程。
  • Dedicated、Shared 和 Service Worker 的共享范围与职责不同。
  • 普通消息默认结构化克隆,大型二进制数据可以使用 Transferable。
  • Worker 不能直接操作 DOM,也需要显式设计错误处理和生命周期清理。