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

显示模式

登录
ARCHIVE DOCUMENTJS

JavaScript 性能利器 —— Web Worker

所属馆藏
JavaScript
文件格式
Markdown
原始路径
JavaScript/25-JavaScript 性能利器 —— Web Worker
本文目录14 个章节
  1. 一、Worker 是什么
  2. 二、适合和不适合的工作
  3. 三、兼容性截图只是历史资料
  4. 四、创建 Dedicated Worker
  5. 五、SharedWorker 与 MessagePort
  6. 六、消息传递与结构化克隆
  7. 七、Transferable:转移所有权而不是复制
  8. 八、内嵌 Worker:Blob URL
  9. 九、关闭和生命周期
  10. 十、错误处理
  11. 十一、加载外部脚本
  12. 十二、Worker 能访问哪些对象
  13. 十三、Worker、Service Worker 与安全策略
  14. 参考资料与原文

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 年左右的环境:

原文历史配图:Dedicated Worker 支持度

原文历史配图:SharedWorker 支持度

截图仅用于保留历史背景,不代表 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 没有 windowdocument 或页面 DOM,但可以使用一部分通用 Web API,例如 fetchWebSocketIndexedDB、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 当成跨域通信工具;跨窗口通信应根据需求选择 postMessageBroadcastChannel 或服务器端消息系统。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:所有权已经转移

常见可转移对象包括 ArrayBufferMessagePortImageBitmap 和部分浏览器支持的 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 的 errormessageerror

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。常见能力包括:

  • selflocationnavigator 的 Worker 版本;
  • postMessageMessagePortMessageChannel
  • setTimeoutsetInterval
  • fetchWebSocketXMLHttpRequest(受浏览器实现和权限影响);
  • IndexedDB、Cache API;
  • importScripts(仅经典 Worker);
  • OffscreenCanvasWeb 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 路径也能运行。

参考资料与原文

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

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

按分类浏览

10 COLLECTIONS