JavaScript 性能利器 —— Web Worker
Category(分类): JavaScript Status: 已整理(2026)
原文写于 2018 年,保留了 Dedicated Worker、SharedWorker、消息传递、结构化克隆、Transferable、关闭和错误处理等主线。浏览器支持率截图属于历史资料,不能继续当作当前兼容性数据;本文以 Web API 语义和现代 Worker 写法为准。
一、Worker 是什么
Web Worker 是浏览器提供的后台执行上下文,用于把计算、数据处理或部分 I/O 协调工作从页面脚本中隔离出来。浏览器通常会使用独立线程运行 Worker,但规范重点是独立的执行上下文和消息通信,不应把“必然是一个操作系统线程”当成 Web API 契约。
常见的 Worker 形态包括:
- Dedicated Worker(专用 Worker):由一个创建者脚本使用,通过
Worker对象通信; - SharedWorker(共享 Worker):同源的多个页面或脚本可以连接同一个共享 Worker,通过不同的
MessagePort通信; - Service Worker:事件驱动、可被浏览器唤醒的网络代理上下文,用于离线缓存、推送等,不是普通计算 Worker 的替代品;
- Worklet:针对渲染、音频等特定流水线的轻量上下文,API 和生命周期与 Worker 不同。
Worker 可以让主线程更专注于交互和渲染,但它不能让 DOM 操作自动变快;主线程仍然需要处理消息、更新 DOM 和绘制。如果消息过于频繁、数据复制过大,通信本身也会成为瓶颈。
二、适合和不适合的工作
适合放入 Worker 的工作通常具有计算量大、输入输出清晰、可以通过消息传递的特点:
- 大型 JSON/CSV/文本解析;
- 图像处理、压缩、编码和加密;
- 复杂排序、搜索、数据聚合;
- 与
OffscreenCanvas配合的部分绘图任务; - 机器学习或其他长时间计算。
不适合直接放入普通 Worker 的工作包括:
- 直接读写页面 DOM;
- 依赖页面事件对象和布局信息的逻辑;
- 很小的计算(创建 Worker 和消息通信的成本可能更高);
- 需要大量同步共享可变对象的算法。
应先测量主线程长任务、消息耗时和内存占用,再决定是否使用 Worker。Worker 不是“性能开关”。
三、兼容性截图只是历史资料
原文中的两张 Can I Use/浏览器支持度截图来自 2018 年左右的环境:


截图仅用于保留历史背景,不代表 2026 年的准确百分比。具体项目应使用目标浏览器矩阵、MDN Baseline、Can I Use 当前数据和实际特性检测;不要在文章中固定写死旧的 93.05% 或 41.66%。
四、创建 Dedicated Worker
主线程可以直接传入脚本 URL:
if ('Worker' in globalThis) {
const worker = new Worker('./worker.js', {
name: 'data-worker'
})
worker.addEventListener('message', event => {
console.log('主线程收到:', event.data)
})
worker.addEventListener('error', event => {
console.error('Worker 执行错误:', event.message)
})
worker.addEventListener('messageerror', event => {
console.error('消息无法反序列化:', event)
})
worker.postMessage({ values: [10, 24] })
}
Worker 的脚本 URL 受安全策略和同源规则约束,不能简单地把任意跨域 URL 当作 Worker 脚本。实际加载还会受到 CSP、模块类型和服务器响应头影响。使用 file:// 直接打开页面经常会遇到 Worker、模块或 CORS 限制,开发时应通过本地 HTTP 服务器访问。
worker.js:
self.addEventListener('message', event => {
const values = event.data.values
const result = values[1] - values[0]
self.postMessage(result)
})
在 Worker 全局作用域中,self 指向 Worker 的全局对象。普通 Worker 没有 window、document 或页面 DOM,但可以使用一部分通用 Web API,例如 fetch、WebSocket、IndexedDB、Cache API 和定时器,具体支持取决于 Worker 类型和浏览器。
使用模块 Worker
模块 Worker 适合使用 import/export 和模块依赖:
const worker = new Worker('./worker.js', {
type: 'module',
name: 'module-worker'
})
worker.postMessage('开始工作')
在使用打包器时,常见写法是:
const worker = new Worker(new URL('./worker.js', import.meta.url), {
type: 'module'
})
模块 Worker 的依赖加载遵循模块脚本和 CORS 规则;它不是把经典 Worker 文件简单地加上 type 字符串就完成了。importScripts() 只适用于经典 Worker,在模块 Worker 中不能使用它。
五、SharedWorker 与 MessagePort
SharedWorker 允许同源的多个浏览上下文连接同一份 Worker 全局上下文。创建者拿到的是 SharedWorker,实际消息需要通过它的 port:
const sharedWorker = new SharedWorker('./shared-worker.js', {
name: 'shared-data-worker'
})
sharedWorker.port.addEventListener('message', event => {
console.log('共享 Worker 返回:', event.data)
})
// 使用 addEventListener 时显式启动端口消息派发
sharedWorker.port.start()
sharedWorker.port.postMessage({ type: 'ping' })
如果使用 port.onmessage = handler,浏览器会隐式启动端口;使用 addEventListener('message', handler) 时,调用 start() 是清晰且兼容历史实现的写法。
shared-worker.js:
self.addEventListener('connect', event => {
const port = event.ports[0]
port.addEventListener('message', messageEvent => {
if (messageEvent.data.type === 'ping') {
port.postMessage({ type: 'pong' })
}
})
port.start()
})
共享 Worker 的连接方必须满足相应的同源和安全条件。不要把 SharedWorker 当成跨域通信工具;跨窗口通信应根据需求选择 postMessage、BroadcastChannel 或服务器端消息系统。SharedWorker 的浏览器支持度也比 Dedicated Worker 更有限,发布前应按目标浏览器验证。
六、消息传递与结构化克隆
主线程和 Worker 之间通过 postMessage() 发送消息,消息在接收端以 message 事件到达。传递的不是同一个普通对象引用,而是通过结构化克隆算法复制可克隆的数据:
const worker = new Worker('./worker.js')
const message = {
numbers: [1, 2, 3],
createdAt: new Date(),
metadata: new Map([['source', 'main']])
}
worker.postMessage(message)
结构化克隆支持比 JSON.stringify 更丰富的类型,也能处理循环引用;但它不会保留普通对象的原型、属性描述符和方法身份。函数、DOM 节点等不可克隆值会导致 DataCloneError(通常表现为 DOMException),不是把函数源码自动传给 Worker:
try {
worker.postMessage({ callback: () => console.log('不能这样传') })
} catch (error) {
console.log(error.name) // DataCloneError
}
现代浏览器可以结构化克隆部分 Error 信息,但不应依赖所有自定义属性和完整堆栈都能跨环境保留。发送方在调用 postMessage 时就发现不可克隆值时,通常会同步抛错;接收端的 messageerror 用于处理消息无法在接收端反序列化等情况。
postMessage 的第一个参数可以是任意可克隆值,不是“只能发送一个对象”。第二个参数可以传输对象列表,也可以在较新的 API 中使用 { transfer: [...] } 选项。
七、Transferable:转移所有权而不是复制
大块二进制数据可以使用 Transferable 对象转移所有权,避免复制。转移 ArrayBuffer 后,发送端的 buffer 会被分离,不能再读取其内容:
const buffer = new ArrayBuffer(1024)
const view = new Uint8Array(buffer)
view[0] = 42
worker.postMessage({ buffer }, [buffer])
console.log(buffer.byteLength) // 0:所有权已经转移
常见可转移对象包括 ArrayBuffer、MessagePort、ImageBitmap 和部分浏览器支持的 OffscreenCanvas。转移前应确认发送端不再需要访问该对象,否则后续读取会失败或得到分离状态。
SharedArrayBuffer 是共享内存而不是 Transferable;它涉及 Atomics 和跨源隔离(通常需要 COOP/COEP 响应头)等额外安全条件,不能把它和普通 ArrayBuffer 的转移混为一谈。
八、内嵌 Worker:Blob URL
页面可以把字符串放进 Blob,再创建一个 Blob URL。原文的 new Blob(workerScript, ...) 参数形式不正确:Blob 的第一个参数应是可迭代的字符串/Buffer 部件数组。
const workerCode = `
self.onmessage = event => {
self.postMessage(event.data * 2)
}
`
const blob = new Blob([workerCode], { type: 'text/javascript' })
const workerUrl = URL.createObjectURL(blob)
const inlineWorker = new Worker(workerUrl)
inlineWorker.onmessage = event => {
console.log(event.data) // 20
inlineWorker.terminate()
}
inlineWorker.postMessage(10)
URL.revokeObjectURL(workerUrl)
调用 revokeObjectURL 会释放页面对 Blob URL 的引用;Worker 已经根据 URL 开始加载后通常可以立即释放,但若要兼容特殊加载流程,也可以在确认 Worker 已加载后再释放。Blob Worker 仍然受 CSP 的 worker-src/child-src 等策略约束,不能用它绕过安全策略。
另一个常见选择是 HTML 中的 <script type="text/plain"> 保存字符串,再通过 Blob 创建 Worker。type="javascript/worker" 并不是浏览器会自动执行的标准脚本类型,页面必须自己读取其文本并创建 Blob。
九、关闭和生命周期
主线程可以关闭 Dedicated Worker:
worker.terminate()
terminate() 会立即终止 Worker,不会等待正在执行的任务完成,也不会执行 Worker 中剩余的清理逻辑。Worker 自己可以调用:
self.close()
SharedWorker 没有与 Dedicated Worker 完全相同的单一 terminate() 入口。连接页面应关闭自己的 MessagePort,当没有活动连接且没有其他保持条件时,浏览器可以回收共享 Worker;具体生命周期由浏览器管理。页面卸载、组件销毁和路由切换时应主动取消监听、关闭端口或终止不再需要的 Dedicated Worker。
不要把 close() 描述成“与 terminate() 完全等效”:前者由 Worker 自己请求关闭,后者由创建者终止 Worker,控制方向和清理时机不同。
十、错误处理
主线程通常监听 Worker 的 error 和 messageerror:
const worker = new Worker('./worker.js')
const renderResult = result => console.log('渲染结果:', result)
worker.addEventListener('error', event => {
console.error('脚本执行失败', event.message, event.filename, event.lineno)
})
worker.addEventListener('messageerror', event => {
console.error('消息接收失败', event)
})
worker.addEventListener('message', event => {
try {
renderResult(event.data)
} catch (error) {
console.error('主线程处理结果失败', error)
}
})
Worker 内部也可以设置 self.onerror 或监听 error。如果 postMessage 的数据在发送端就不可克隆,异常可能直接从发送调用抛出,而不是先进入 Worker 的 onerror。
self.addEventListener('error', event => {
console.error('Worker 内部错误:', event.message)
})
self.addEventListener('unhandledrejection', event => {
console.error('Worker 未处理的 Promise 拒绝:', event.reason)
})
十一、加载外部脚本
经典 Worker 可以同步调用 importScripts():
importScripts('parser.js', 'decoder.js')
这不是动态并发加载接口,调用会阻塞当前 Worker 的脚本执行;模块 Worker 应使用静态 import 或动态 import():
import { parse } from './parser.js'
self.onmessage = event => {
self.postMessage(parse(event.data))
}
Worker 可以创建子 Worker,但脚本 URL 和安全策略仍需满足相应条件;URL 相对哪个脚本解析,也要以当前 Worker 脚本的基准 URL 为准,不要只凭页面 URL 猜测。
十二、Worker 能访问哪些对象
Worker 没有页面 window 和 DOM,但有自己的全局对象和部分环境 API。常见能力包括:
self、location、navigator的 Worker 版本;postMessage、MessagePort、MessageChannel;setTimeout、setInterval;fetch、WebSocket、XMLHttpRequest(受浏览器实现和权限影响);IndexedDB、Cache API;importScripts(仅经典 Worker);OffscreenCanvas、Web Crypto等部分 API。
Worker 也有自己的事件循环和任务队列。它不是页面主线程的“第二个 DOM 线程”,不能直接修改页面布局。主线程和 Worker 之间应该通过消息传递明确数据边界。
十三、Worker、Service Worker 与安全策略
Service Worker 的生命周期、事件模型和缓存代理能力与 Dedicated/Shared Worker 不同。它可以拦截网络请求,但不能作为页面中任意长计算的普通 Worker 使用。选择之前先确认需求是:
- 一次页面生命周期内的后台计算:Dedicated Worker;
- 多个同源页面共享连接或状态:SharedWorker(并确认支持度);
- 离线缓存、推送、网络代理:Service Worker;
- 特定渲染/音频阶段:对应 Worklet。
Worker 脚本和 Blob URL 还会受到 CSP、同源策略、模块 CORS、COOP/COEP 等安全配置影响。开发环境能运行不代表生产环境的响应头和 CDN 路径也能运行。