先记住这个答案
浏览器中的 Workers 主要有三种。第一是专用 Worker(Dedicated Worker),由单个页面脚本创建并独占,适合做 CPU 密集计算,如图像处理、大数据解析,随页面关闭而销毁。第二是共享 Worker(Shared Worker),同源下多个标签页或 iframe 通过 port 连接同一个实例,适合跨页面共享状态或单一 WebSocket 连接,所有页面断开后才终止。第三是 Service Worker,它是浏览器代理层,独立于页面运行,能拦截 fetch、做离线缓存、推送和后台同步,有自己的安装、激活生命周期,页面全关后仍可被事件唤醒。选型核心:算力卸载用专用 Worker,跨标签页共享用共享 Worker,网络拦截与离线用 Service Worker。
- 专用 Worker 做单页面后台计算
- 共享 Worker 供同源多标签页共用
- Service Worker 拦截网络并支持离线
- 三者生命周期与归属方完全不同
三类 Worker 的机制与生命周期差异
三类 Worker 都运行在独立于主线程的线程上,通过 postMessage 和 onmessage 通信,数据默认按结构化克隆拷贝而非共享。专用 Worker 由 new Worker(url) 创建,只被创建它的脚本持有,全局作用域是 DedicatedWorkerGlobalScope,主线程调用 terminate() 或页面卸载即结束。它解决的是主线程阻塞问题:把耗时循环、解析、编解码挪到后台线程,UI 保持可响应。
共享 Worker 用 new SharedWorker(url) 创建,同源下第二个标签页再创建指向同一脚本 URL 的实例时会复用已有线程,各方通过 port 收发消息,全局作用域是 SharedWorkerGlobalScope,当最后一个连接端口关闭后才销毁。Service Worker 则完全不同:通过 navigator.serviceWorker.register() 注册,经历 install、activate 等阶段,浏览器可在页面全关后因 push、sync 事件唤醒它,全局作用域是 ServiceWorkerGlobalScope,核心能力是作为页面与网络之间的可编程代理。
function makeRegistry() {
const registry = { dedicated: [], shared: new Map(), service: null };
return registry;
}
function spawnDedicated(registry, ownerId) {
const w = { owner: ownerId, alive: true };
registry.dedicated.push(w);
return w;
}
function connectShared(registry, name, clientId) {
if (!registry.shared.has(name)) {
registry.shared.set(name, { alive: true, ports: new Set() });
}
registry.shared.get(name).ports.add(clientId);
return registry.shared.get(name);
}
function disconnectShared(registry, name, clientId) {
const inst = registry.shared.get(name);
inst.ports.delete(clientId);
if (inst.ports.size === 0) inst.alive = false;
return inst.alive;
}
const reg = makeRegistry();
const w1 = spawnDedicated(reg, 'tab-A');
const w2 = spawnDedicated(reg, 'tab-B');
connectShared(reg, 'sync', 'tab-A');
connectShared(reg, 'sync', 'tab-B');
const afterOneClose = disconnectShared(reg, 'sync', 'tab-A');
const afterAllClose = disconnectShared(reg, 'sync', 'tab-B');
reg.service = { registered: true, pageDependent: false };
console.log(JSON.stringify({
dedicatedOwners: [w1.owner, w2.owner],
sharedAliveAfterOneClose: afterOneClose,
sharedAliveAfterAllClose: afterAllClose,
serviceSurvivesPageClose: !reg.service.pageDependent
}));查看输出与解释
{"dedicatedOwners":["tab-A","tab-B"],"sharedAliveAfterOneClose":true,"sharedAliveAfterAllClose":false,"serviceSurvivesPageClose":true}用纯数据模拟三类 Worker 的归属规则:专用 Worker 各属其主,共享 Worker 在仍有端口连接时存活,Service Worker 不依赖页面。真实 Worker 需在浏览器中用对应构造函数创建。
图片批处理上传页该选哪种 Worker
场景:一个相册后台页面,用户一次拖入 200 张原图,需要在浏览器内压缩、生成缩略图后再上传,约束是压缩期间页面必须可滚动、可取消操作。决策:用专用 Worker 配合 OffscreenCanvas,每张图压缩为独立任务,主线程发图、Worker 返回 Blob。结果:主线程帧率不受影响,用户点取消时主线程直接 terminate() 丢弃队列,无需 Worker 内部清理复杂状态。
不选共享 Worker 的原因:压缩结果只服务当前页面,跨标签页复用反而引入端口管理和并发争抢;不选 Service Worker 的原因:它不面向长时计算,且可能被浏览器随时终止,任务中途丢失没有进度保障。若需求变成『多个后台页面共用一条 WebSocket 同步压缩进度』,才把连接层挪进共享 Worker,计算仍留在各页面的专用 Worker 里,这是典型的分层组合用法。
什么情况下这些 Worker 会失效或帮不上忙
共享 Worker 的失效条件更隐蔽:不同源无法共享;部分浏览器(如早期移动端 Safari)不支持 SharedWorker,必须做 'SharedWorker' in window 检测并降级为专用 Worker 加 BroadcastChannel。
对应处理与代价:不支持的浏览器走特性检测降级路径,代价是要维护两套通信封装。Service Worker 的缓存策略一旦写错会长期滞留旧资源,因为更新受浏览器定期检查和 skipWaiting 逻辑影响,修复成本是发版后仍可能有用户停留在旧版本。另外三类 Worker 都不能传函数或 DOM 节点,违反结构化克隆限制会抛 DataCloneError,设计消息协议时要把数据约束为可克隆结构。
容易答错的地方
- Service Worker 也是一种后台计算线程
- 错误。Service Worker 的定位是网络代理与离线能力,事件驱动、随时可能被浏览器终止,不适合承载长时 CPU 计算;算力卸载应选专用 Worker。
- 共享 Worker 能跨不同源共享数据
- 错误。共享 Worker 要求所有连接方与 Worker 脚本同源,跨源页面各自创建的是不同实例;跨源通信需借助 iframe 加
postMessage等其他机制。
面试官还会怎么问?
三类 Worker 之间传递的数据是共享内存吗?
默认不是。postMessage 走结构化克隆,接收方拿到拷贝。只有显式使用 SharedArrayBuffer 加跨源隔离,或传 Transferable 对象转移所有权时,才接近共享或零拷贝语义。
Worker 里能操作 DOM 吗?
不能。三类 Worker 都没有 window 和 document,无法直接读写 DOM。需要 UI 更新时把结果消息发回主线程处理;例外是 OffscreenCanvas 允许在 Worker 中渲染画布。
页面关闭后哪种 Worker 还能继续工作?
只有 Service Worker。专用 Worker 随页面销毁,共享 Worker 在最后一个连接断开后终止,Service Worker 由浏览器管理,可被 push、sync 等事件在页面全关后唤醒。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。