可以不会用但你必须要了解的 Web Worker 详解
Category(分类): JavaScript Status: 已整理
本文保留原文关于 Worker 的入门顺序和示例主题,并修正抓取排版、过时 API 和过度绝对化的结论。示例默认运行在浏览器;Node.js 的
worker_threads不是 Web Worker API,不能直接套用本文代码。
简介
在同一个 JavaScript agent 中,一段正在运行的同步代码会一直执行到当前调用栈清空,期间不会被另一个 JavaScript 任务抢占。长时间计算因此会阻塞该 agent 的输入、脚本和渲染工作。
Web Worker 是 HTML 宿主提供的后台脚本能力。页面可以创建另一个独立的 JavaScript agent,让计算在另一个 Worker 全局环境中进行。浏览器可以把不同 agent 调度到不同的操作系统线程上并行执行,但“一个 Worker 一定对应一个 OS 线程”不是 Web 标准保证。
JavaScript 语言规范本身不规定浏览器必须使用怎样的线程模型。更准确的说法是:一个 agent 内的 JavaScript 执行具有 run-to-completion 特征;多个 Worker 可以提供并发执行能力。 Worker 不能直接访问创建页面的 DOM,也不会与页面共享普通的词法作用域。
Web 平台常见的 Worker/相关模型如下:
| 类型 | 创建方式 | 典型用途 | 通信与生命周期 |
|---|---|---|---|
| Dedicated Worker(专用 Worker) | new Worker(url) | 某个页面或 Worker 的后台计算 | 通过 Worker 对象通信,通常由创建者管理 |
| Shared Worker(共享 Worker) | new SharedWorker(url) | 同源多个页面共享一份状态 | 通过 MessagePort 连接;多个页面必须满足同源条件 |
| Service Worker(服务 Worker) | navigator.serviceWorker.register(url) | 缓存、离线、请求拦截、推送 | 按 scope 和事件启动,可能在空闲时被浏览器终止;不是通用计算 Worker |
本文主要讨论前两种。Service Worker 会在后文单独说明。
应用场景
Worker 适合把足够耗时、可拆分且不依赖 DOM 的任务移出页面 agent,例如:
- 数学运算、加密或压缩
- 大量 JSON/文本解析
- 图像处理、音视频数据处理
- 使用
OffscreenCanvas的图形计算 - 大数据筛选、排序和索引构建
- 复杂格式转换
Worker 不是免费的线程池。创建 Worker 有启动和内存成本,Worker 也会与页面竞争 CPU、电量和内存;频繁发送大量小消息还会增加主线程任务。实际项目应测量后决定是否使用 Worker,并考虑 Worker 池、批处理和任务取消。
使用前的注意事项
- 普通 Worker 没有
window和 DOM,不能读取或修改页面节点;可以使用的 API 取决于 Worker 类型和浏览器,常见能力包括fetch、WebSocket、IndexedDB、Cache、Web Streams、crypto、structuredClone和部分 Canvas API。 - 主线程和 Worker 之间通常通过
postMessage()、MessagePort、MessageChannel或BroadcastChannel通信。postMessage()默认使用结构化克隆,而不是共享同一个普通对象实例。 ArrayBuffer、MessagePort、OffscreenCanvas、ImageBitmap等对象可以在支持的场景下转移所有权;SharedArrayBuffer则是在满足跨源隔离条件后共享底层数据。复制、转移和共享是三种不同的语义。- Worker 不直接占用页面的 event loop,但它的计算、消息回调和资源使用仍可能间接影响页面性能。频繁消息会让主线程持续处理 task。
- Dedicated Worker 的脚本地址受 URL、同源/CORS、CSP
worker-src等条件影响;正式页面应使用 HTTPS 或 localhost,并通过能力检测处理兼容性。
专用 Worker(Dedicated Worker)
创建一个模块 Worker
现代代码可以使用模块 Worker。下面是两个独立文件,主线程和 Worker 文件不能拼接在同一个代码块中。
主线程 main.js:
const worker = new Worker(new URL('./worker.js', import.meta.url), {
type: 'module',
name: 'sum-worker'
})
worker.addEventListener('message', ({ data }) => {
console.log('Worker result:', data)
})
worker.addEventListener('error', (event) => {
console.error('Worker runtime error:', {
message: event.message,
filename: event.filename,
line: event.lineno,
column: event.colno,
error: event.error
})
})
worker.addEventListener('messageerror', () => {
console.error('The message could not be deserialized')
})
worker.postMessage({ values: [10, 24] })
Worker 文件 worker.js:
self.addEventListener('message', ({ data }) => {
const values = Array.isArray(data?.values) ? data.values : []
const result = values.reduce((sum, value) => sum + Number(value), 0)
self.postMessage({ result })
})
主线程输出 { result: 34 }。addEventListener() 可以注册多个监听器;onmessage = handler 是方便的单个事件处理器属性,重复赋值会覆盖之前的处理器。现代模块 Worker 中不要依赖顶层 this,使用 self 或事件目标更清晰。
如果项目使用构建工具,new URL('./worker.js', import.meta.url) 通常能让工具正确处理 Worker 资源;直接部署原生 HTML 时,也可以传入可访问的 /worker.js URL。
消息与结构化克隆
主线程和 Worker 中的对象不是同一个实例。下面的对象会被结构化克隆,Worker 对副本的修改不会改变主线程的原对象:
主线程:
const worker = new Worker('./worker.js')
const payload = {
numbers: [1, 2, 3],
nested: { enabled: true }
}
worker.onmessage = ({ data }) => {
console.log(data) // { numbers: [2, 4, 6], nested: { enabled: true } }
console.log(payload.numbers) // [1, 2, 3]
}
worker.postMessage(payload)
Worker:
self.onmessage = ({ data }) => {
data.numbers = data.numbers.map((value) => value * 2)
self.postMessage(data)
}
结构化克隆支持很多内建类型和循环引用,但并不是“完整复制任意 JavaScript 值”:函数、DOM 节点等通常不能克隆,会抛出 DataCloneError;类实例的原型链、属性描述符和私有状态也不会按原样保留。发送前应把数据设计成明确的可序列化协议。
转移所有权(Transferable objects)
某些对象可以放进 postMessage 的第二个参数或 options.transfer 中。转移后,发送端失去该资源的所有权;对 ArrayBuffer 来说,发送端的 buffer 会被 detach。这里转移的是 Uint8Array.buffer,不是 Uint8Array 视图本身:
const worker = new Worker('./worker.js')
const bytes = new Uint8Array(32 * 1024 * 1024)
worker.postMessage({ buffer: bytes.buffer }, [bytes.buffer])
console.log(bytes.buffer.byteLength) // 0:发送端的 ArrayBuffer 已 detach
Worker 端可以接收并使用这个 ArrayBuffer:
self.onmessage = ({ data }) => {
const bytes = new Uint8Array(data.buffer)
bytes[0] = 255
self.postMessage({ byteLength: bytes.byteLength })
}
“转移”通常可以避免复制大块数据,但不能把所有 Transferable 都宣传成绝对 zero-copy。还要注意:被转移的 ArrayBuffer 不能再由发送端读取;如果还需要两端同时访问,应考虑结构化克隆或满足条件后使用 SharedArrayBuffer。
SharedArrayBuffer 与跨源隔离
SharedArrayBuffer 允许多个 agent 看到同一块共享内存,必须配合 Atomics 和明确的同步协议。现代浏览器通常要求页面是安全上下文且 crossOriginIsolated === true,常见响应头包括:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
COEP: credentialless 是另一种部署选择,但会影响跨源脚本、图片、iframe 等资源。启用前应检查第三方资源并考虑先使用 Report-Only。
浏览器代码可以先做能力检查:
if (!crossOriginIsolated || typeof SharedArrayBuffer === 'undefined') {
throw new Error('SharedArrayBuffer is not available in this context')
}
const shared = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT)
const state = new Int32Array(shared)
const worker = new Worker('./shared-worker.js')
worker.postMessage({ shared })
Atomics.store(state, 0, 1)
Atomics.notify(state, 0)
Worker:
self.onmessage = ({ data }) => {
const state = new Int32Array(data.shared)
Atomics.wait(state, 0, 0)
self.postMessage({ value: Atomics.load(state, 0) })
}
SharedArrayBuffer 不应放进 transfer list;它的意义就是共享底层数据。共享内存会重新引入竞态、死锁和活锁等问题,Worker 隔离并不自动保证线程安全。
关闭 Worker
主线程可以立即终止 Worker:
worker.terminate()
terminate() 不会给 Worker 留出清理机会。更稳妥的协议是先发送 shutdown 消息,让 Worker 停止接收新任务并释放资源,必要时再调用 terminate()。Worker 内部可以调用 self.close() 关闭自己的全局环境:
self.addEventListener('message', ({ data }) => {
if (data === 'shutdown') {
self.close()
}
})
错误处理
运行时未捕获异常通常触发 error 事件;消息无法反序列化时触发 messageerror。两者不是同一种错误:
worker.addEventListener('error', (event) => {
event.preventDefault()
console.error(event.message, event.filename, event.lineno, event.colno)
})
worker.addEventListener('messageerror', (event) => {
console.error('Message deserialization failed', event)
})
error 事件可以读取 message、filename、lineno、colno 和部分环境提供的 error。preventDefault() 是否阻止控制台默认报告属于宿主行为,不能替代应用自己的错误记录。
生成子 Worker
Worker 可以创建子 Worker。子 Worker 的脚本 URL 通常相对于父 Worker 脚本的 URL 解析,并受同源、CSP 和模块 CORS 规则限制。不要把“只要同源就一定能创建成功”写成绝对保证,应结合部署策略和浏览器能力检测。
引入脚本与库:importScripts() 的边界
importScripts() 是 classic Worker 的同步导入 API,参数按顺序执行:
// classic-worker.js
importScripts('./config.js', './math.js')
它不能在 module Worker 中使用;模块 Worker 应使用静态 import:
// module-worker.js
import { add } from './math.js'
self.onmessage = ({ data }) => {
self.postMessage(add(data.a, data.b))
}
同步导入会阻塞当前 Worker 的后续脚本执行,模块 Worker 通常更适合现代依赖管理。生产环境还要检查 CSP worker-src、脚本的 MIME 类型和跨源配置。
嵌入式 Worker:Blob URL
没有 src 且 MIME 类型不是可执行 JavaScript 的 script 元素,可以作为文本容器,再通过 Blob 生成 Worker 脚本。这是一个方便的演示技巧,生产环境应优先使用独立的模块文件:
<script id="worker-source" type="text/plain">
self.onmessage = ({ data }) => {
self.postMessage(data * data)
}
</script>
<script>
const source = document.querySelector('#worker-source').textContent
const blob = new Blob([source], { type: 'text/javascript' })
const objectUrl = URL.createObjectURL(blob)
const worker = new Worker(objectUrl)
worker.onmessage = ({ data }) => {
console.log(data)
worker.terminate()
URL.revokeObjectURL(objectUrl)
}
worker.postMessage(6)
</script>
Blob 的参数应是 BlobPart 数组。Blob Worker 还会受到 CSP worker-src 是否允许 blob: 的影响,使用完毕要 terminate() 并 revokeObjectURL()。
Worker 上下文(WorkerGlobalScope)
普通 Worker 运行在不同于页面 Window 的全局环境中:
- 没有
window、DOM、document和页面的 CSSOM; self指向当前 Worker 的全局对象;- 可用 API 依 Worker 类型和浏览器而异,常见有
fetch、WebSocket、IndexedDB、Cache、crypto、console、performance、定时器和部分 Canvas API; - 专用 Worker 使用
DedicatedWorkerGlobalScope,共享 Worker 使用SharedWorkerGlobalScope;页面的Navigator/Location对应 Worker 环境中的WorkerNavigator/WorkerLocation。
原文中 WindowTimers、WindowBase64、Firefox OS Data Store 和 ChromeWorker 属于历史接口或特权扩展环境,不应当作为当前网页 Worker 的通用能力列表。
共享 Worker(Shared Worker)
共享 Worker 可以被多个同源页面或脚本连接。创建者拿到的是 SharedWorker 对象,实际通信要使用它的 port:
页面端:
const sharedWorker = new SharedWorker('./shared-worker.js', {
name: 'counter'
})
sharedWorker.port.addEventListener('message', ({ data }) => {
console.log('SharedWorker:', data)
})
sharedWorker.port.start()
sharedWorker.port.postMessage({ type: 'increment' })
共享 Worker 端:
let count = 0
self.onconnect = ({ ports }) => {
const port = ports[0]
port.addEventListener('message', ({ data }) => {
if (data?.type === 'increment') count += 1
port.postMessage({ count })
})
port.start()
}
调用 port.start() 后才开始接收通过 addEventListener 注册的消息;如果使用 port.onmessage = handler,浏览器会隐式启动端口。多个连接必须满足同源条件,SharedWorker 的支持情况也应以目标浏览器矩阵为准。
Service Worker 不是普通计算 Worker
Service Worker 通过注册和 scope 管理页面网络代理:
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js', { scope: '/' })
.then((registration) => console.log('registered', registration.scope))
.catch((error) => console.error('registration failed', error))
}
它通常要求 HTTPS(localhost 等安全开发环境例外),具有 install、activate、fetch 等事件生命周期,没有 DOM,且可能在空闲时被浏览器终止,不能假设顶层变量永久保存。Service Worker 适合缓存和请求拦截,不应当被当作长期驻留的计算线程。
关于线程安全
默认结构化克隆和 DOM 隔离降低了共享可变状态的风险,但不等于“Worker 天然线程安全”。多个 Worker 仍会竞争 CPU/内存;SharedWorker 的多个端口共享同一 Worker 状态;SharedArrayBuffer 和 Atomics 还允许显式共享内存。使用共享状态时仍需设计锁、原子操作、消息协议、取消和错误传播。
其它相关模型
- Service Worker:见上一节,用于网络代理、离线、推送和后台同步。
- AudioWorklet:Web Audio 的音频实时处理模型,属于 worklet,不是“Audio Worker”这一通用 Worker 类型。
- Worklet:CSS Paint、Animation 等专用执行模型,能力和生命周期由各自规范定义。
- ChromeWorker / js-ctypes:Firefox 特权扩展环境的历史 API,不属于普通网页 Web Worker 教程范围。
小结
- Worker 提供独立 agent 和后台执行能力,但浏览器的 OS 线程映射是实现细节。
- 默认消息传递使用结构化克隆;Transferable 转移所有权;SharedArrayBuffer 在隔离条件下共享内存。
- 模块 Worker 优先使用
import,classic Worker 才使用importScripts()。 terminate()、close()、Service Worker 的生命周期语义不同,不能混用。- Worker 能降低主线程长任务压力,但不会消除 CPU、内存、通信和同步成本。


配图说明:两张图片按原文顺序从掘金 CDN 下载到 images/89-image-*,原始作者和再分发许可未能从页面确认;它们仅作为历史配图保留,公开发布前应核实授权或替换为自制图。
参考资料
- WHATWG HTML:Web Workers
- WHATWG:结构化数据与 Transferable objects
- MDN:Web Workers API
- MDN:Using Web Workers
- MDN:Worker constructor
- MDN:Structured clone algorithm
- MDN:Transferable objects
- MDN:Service Worker API
- MDN:SharedArrayBuffer
- MDN:AudioWorklet
- MDN:crossOriginIsolated
- JavaScript 性能利器 —— Web Worker(历史来源)
- 浅谈 HTML5 Web Worker(历史来源)
- 原文:可以不会用但你必须要了解的 Web Worker 详解
作者:碎_浪 原文链接:https://juejin.cn/post/6844904158101766151 来源:稀土掘金。