技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTJS

可以不会用但你必须要了解的 Web Worker 详解

所属馆藏
JavaScript
文件格式
Markdown
原始路径
JavaScript/89-可以不会用但你必须要了解的Web Worker详解
本文目录10 个章节
  1. 简介
  2. 应用场景
  3. 专用 Worker(Dedicated Worker)
  4. Worker 上下文(WorkerGlobalScope)
  5. 共享 Worker(Shared Worker)
  6. Service Worker 不是普通计算 Worker
  7. 关于线程安全
  8. 其它相关模型
  9. 小结
  10. 参考资料

可以不会用但你必须要了解的 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 池、批处理和任务取消。

使用前的注意事项

  1. 普通 Worker 没有 window 和 DOM,不能读取或修改页面节点;可以使用的 API 取决于 Worker 类型和浏览器,常见能力包括 fetch、WebSocket、IndexedDB、Cache、Web Streams、cryptostructuredClone 和部分 Canvas API。
  2. 主线程和 Worker 之间通常通过 postMessage()MessagePortMessageChannelBroadcastChannel 通信。postMessage() 默认使用结构化克隆,而不是共享同一个普通对象实例。
  3. ArrayBufferMessagePortOffscreenCanvasImageBitmap 等对象可以在支持的场景下转移所有权;SharedArrayBuffer 则是在满足跨源隔离条件后共享底层数据。复制、转移和共享是三种不同的语义。
  4. Worker 不直接占用页面的 event loop,但它的计算、消息回调和资源使用仍可能间接影响页面性能。频繁消息会让主线程持续处理 task。
  5. 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 事件可以读取 messagefilenamelinenocolno 和部分环境提供的 errorpreventDefault() 是否阻止控制台默认报告属于宿主行为,不能替代应用自己的错误记录。

生成子 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 类型和浏览器而异,常见有 fetchWebSocketIndexedDBCachecryptoconsoleperformance、定时器和部分 Canvas API;
  • 专用 Worker 使用 DedicatedWorkerGlobalScope,共享 Worker 使用 SharedWorkerGlobalScope;页面的 Navigator/Location 对应 Worker 环境中的 WorkerNavigator/WorkerLocation

原文中 WindowTimersWindowBase64、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 等安全开发环境例外),具有 installactivatefetch 等事件生命周期,没有 DOM,且可能在空闲时被浏览器终止,不能假设顶层变量永久保存。Service Worker 适合缓存和请求拦截,不应当被当作长期驻留的计算线程。

关于线程安全

默认结构化克隆和 DOM 隔离降低了共享可变状态的风险,但不等于“Worker 天然线程安全”。多个 Worker 仍会竞争 CPU/内存;SharedWorker 的多个端口共享同一 Worker 状态;SharedArrayBufferAtomics 还允许显式共享内存。使用共享状态时仍需设计锁、原子操作、消息协议、取消和错误传播。

其它相关模型

  • Service Worker:见上一节,用于网络代理、离线、推送和后台同步。
  • AudioWorklet:Web Audio 的音频实时处理模型,属于 worklet,不是“Audio Worker”这一通用 Worker 类型。
  • Worklet:CSS Paint、Animation 等专用执行模型,能力和生命周期由各自规范定义。
  • ChromeWorker / js-ctypes:Firefox 特权扩展环境的历史 API,不属于普通网页 Web Worker 教程范围。

小结

  1. Worker 提供独立 agent 和后台执行能力,但浏览器的 OS 线程映射是实现细节。
  2. 默认消息传递使用结构化克隆;Transferable 转移所有权;SharedArrayBuffer 在隔离条件下共享内存。
  3. 模块 Worker 优先使用 import,classic Worker 才使用 importScripts()
  4. terminate()close()、Service Worker 的生命周期语义不同,不能混用。
  5. Worker 能降低主线程长任务压力,但不会消除 CPU、内存、通信和同步成本。

原文配图:Web Worker 相关插画

原文配图:文章结尾装饰图

配图说明:两张图片按原文顺序从掘金 CDN 下载到 images/89-image-*,原始作者和再分发许可未能从页面确认;它们仅作为历史配图保留,公开发布前应核实授权或替换为自制图。

参考资料

作者:碎_浪 原文链接:https://juejin.cn/post/6844904158101766151 来源:稀土掘金。

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS