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

显示模式

登录
ARCHIVE DOCUMENTHTML

Web Worker 详解

所属馆藏
HTML
文件格式
Markdown
原始路径
HTML/04-HTML Web Worker
本文目录18 个章节
  1. 目录
  2. 一、为什么需要 Web Worker
  3. 二、Web Worker 是什么
  4. 三、Web Worker 的限制
  5. 四、创建和使用 Dedicated Worker
  6. 五、Worker 线程内部代码
  7. 六、消息通信
  8. 七、错误处理
  9. 八、终止 Worker 和取消任务
  10. 九、加载其他脚本
  11. 十、结构化克隆与 Transferable
  12. 十一、模块 Worker
  13. 十二、在 Vite、Vue 和 Nuxt 中使用
  14. 十三、适用场景与不适用场景
  15. 十四、Web Worker、Shared Worker 与 Service Worker
  16. 十五、性能和安全注意事项
  17. 十六、总结
  18. 参考资料

Web Worker 详解

Category(分类): HTML Status: 未知

本文对原文进行了重新整理和勘误。重点说明 Web Worker 的执行模型、限制、通信方式、错误处理、数据传输和现代构建工具用法。原文图片地址已经失效,未保留失效图片链接。

目录

一、为什么需要 Web Worker

浏览器页面主线程通常负责:

  • 执行页面 JavaScript;
  • 处理点击、输入和滚动事件;
  • 计算样式和布局;
  • 更新 DOM;
  • 绘制页面。

如果主线程连续执行大量计算,就可能长时间无法处理用户输入和绘制任务,页面会出现卡顿。常见的耗时任务包括:

  • 大量数据排序、过滤和统计;
  • 加密、解密、哈希和压缩;
  • 复杂文本解析;
  • 图像像素处理;
  • 路径规划、数据分析和算法计算。

Web Worker 可以创建独立的 JavaScript 执行上下文,把适合的计算任务移出页面主线程。主线程和 Worker 可以通过消息交换数据,从而降低计算对页面交互的影响。

需要注意:

Web Worker 不是让同一个 JavaScript 执行上下文变成多线程,而是创建了另一个独立执行上下文。浏览器通常会安排它在独立线程执行,但具体线程调度属于浏览器实现细节。

JavaScript 的“单线程”应理解为:每个 JavaScript 执行上下文通常是单线程的,并不代表整个浏览器只有一个线程。

二、Web Worker 是什么

Web Worker 是浏览器提供的后台 JavaScript 执行环境。Dedicated Worker(专用 Worker)由某个页面或 Worker 创建,通常只被创建者使用。

Worker 与创建它的页面具有不同的全局对象和执行上下文:

页面主线程                         Worker
┌────────────────┐               ┌────────────────┐
│ window         │               │ self           │
│ document / DOM │  postMessage  │ 无页面 DOM      │
│ UI / 渲染       │◄────────────►│ 计算任务        │
└────────────────┘               └────────────────┘

Worker 可以独立接收消息、执行任务并发送结果,但不能直接修改页面 DOM。

Worker 创建后可以持续等待消息,但并不保证永久运行:

  • 主线程可以调用 terminate()
  • Worker 可以调用 self.close()
  • 页面关闭后,关联的 Worker 通常也会结束;
  • 浏览器可能冻结或回收后台页面中的资源;
  • Worker 代码发生未处理错误时,任务可能终止。

三、Web Worker 的限制

3.1 不能直接访问 DOM

Worker 中没有页面的 windowdocument,不能直接执行:

// Worker 中不可用
window.document.querySelector('#app')

如果需要更新页面,应将计算结果发送给主线程:

// Worker
self.postMessage({
  type: 'RESULT',
  value: 42
})

由页面接收消息后操作 DOM。

3.2 可以访问部分 Web API

Worker 中通常可以使用:

  • self
  • navigator
  • location(表示 Worker 脚本的地址);
  • fetch()
  • XMLHttpRequest
  • WebSocket
  • IndexedDB
  • Web Crypto
  • setTimeout()setInterval()

不能直接使用依赖页面窗口的 API,例如 alert()confirm() 和 DOM 相关 API。

3.3 脚本 URL 需要满足安全策略

直接使用 new Worker(url) 时,脚本通常需要与创建它的页面同源,并受 CSP 等安全策略限制。构建工具会把 Worker 文件处理成适合浏览器加载的资源。

不要直接双击 HTML 文件并使用 file:// 测试 Worker。建议通过本地 HTTP 服务器运行项目。

Worker 也可以使用 blob:data: URL,但这不代表可以绕过浏览器的安全策略。

3.4 通信不是普通引用共享

页面和 Worker 默认通过结构化克隆算法传递数据,不共享普通对象的引用。函数、DOM 节点等不可克隆对象不能直接传递。

对于大数据,应考虑 Transferable,避免不必要的数据复制,详见结构化克隆与 Transferable

四、创建和使用 Dedicated Worker

4.1 检查浏览器支持

if (typeof Worker === 'undefined') {
  console.warn('当前环境不支持 Web Worker')
}

现代浏览器普遍支持 Dedicated Worker,但在 SSR 环境中不能直接访问 Worker,需要只在客户端创建。

4.2 创建 Worker

最简单的写法:

const worker = new Worker('./worker.js')

Worker() 的参数是 Worker 脚本 URL。更推荐在现代构建工具中使用 new URL() 写法,后文会介绍。

4.3 主线程发送消息

const worker = new Worker('./worker.js')

worker.postMessage('Hello Worker')
worker.postMessage({
  type: 'ECHO',
  value: 'hello'
})

消息可以是字符串、数字、数组、对象、TypedArray 等可结构化克隆的数据。

4.4 主线程接收消息

worker.addEventListener('message', (event) => {
  console.log('收到 Worker 消息:', event.data)
})

也可以使用属性形式:

worker.onmessage = ({ data }) => {
  console.log('收到 Worker 消息:', data)
}

建议为消息设计统一格式:

worker.postMessage({
  id: crypto.randomUUID(),
  type: 'CALCULATE',
  payload: {
    numbers: [1, 2, 3]
  }
})

使用 id 可以在多个任务同时执行时匹配请求和响应。

五、Worker 线程内部代码

下面是一个完整的 worker.js 示例:

self.addEventListener('message', (event) => {
  const { id, type, payload } = event.data || {}

  if (type === 'ECHO') {
    self.postMessage({
      id,
      type: 'RESULT',
      payload: payload.value
    })
    return
  }

  if (type === 'SUM') {
    const numbers = Array.isArray(payload?.numbers)
      ? payload.numbers
      : []

    const result = numbers.reduce((sum, value) => sum + value, 0)

    self.postMessage({
      id,
      type: 'RESULT',
      payload: result
    })
    return
  }

  self.postMessage({
    id,
    type: 'ERROR',
    error: `未知任务类型:${type}`
  })
})

self 表示 Worker 的全局对象。下面三种写法都可以注册消息监听器:

self.addEventListener('message', handler)
this.addEventListener('message', handler)
addEventListener('message', handler)

推荐使用 self.addEventListener(),语义更清晰,也更适合模块化代码。

Worker 也可以使用 self.onmessage

self.onmessage = ({ data }) => {
  self.postMessage(`收到:${data}`)
}

六、消息通信

6.1 消息是异步的

postMessage() 不会直接调用对方函数,而是把消息放入对方的事件队列中。对方需要通过 message 事件接收。

// 主线程
worker.postMessage({
  type: 'START'
})

// Worker
self.addEventListener('message', ({ data }) => {
  if (data.type === 'START') {
    self.postMessage({
      type: 'STARTED'
    })
  }
})

6.2 使用 MessageChannel

当页面需要与多个 Worker 或多个模块建立独立通信通道时,可以使用 MessageChannel

const channel = new MessageChannel()
const worker = new Worker('./worker.js')

worker.postMessage(
  { type: 'CONNECT' },
  [channel.port2]
)

channel.port1.onmessage = ({ data }) => {
  console.log('收到端口消息:', data)
}

channel.port1.postMessage({ type: 'PING' })

Worker 中接收端口:

self.onmessage = (event) => {
  if (event.data?.type !== 'CONNECT') {
    return
  }

  const port = event.ports[0]

  port.onmessage = ({ data }) => {
    if (data.type === 'PING') {
      port.postMessage({ type: 'PONG' })
    }
  }

  port.start()
}

6.3 消息错误

当消息无法被接收方反序列化时,可以监听 messageerror

worker.addEventListener('messageerror', (event) => {
  console.error('消息无法反序列化:', event)
})

七、错误处理

7.1 主线程监听 Worker 错误

原文中的 worker.onerror(function () {}) 写法是错误的。正确写法如下:

const worker = new Worker('./worker.js')

worker.addEventListener('error', (event) => {
  console.error('Worker 发生错误:', {
    message: event.message,
    filename: event.filename,
    line: event.lineno,
    column: event.colno
  })
})

也可以使用属性形式:

worker.onerror = (event) => {
  console.error(event.message)
}

7.2 Worker 内部监听错误

Worker 内部可以监听自身未捕获的错误:

self.addEventListener('error', (event) => {
  console.error('Worker 内部错误:', event.message)
})

如果需要将错误传回主线程,建议使用统一消息格式:

try {
  const result = doExpensiveWork()
  self.postMessage({
    type: 'RESULT',
    payload: result
  })
} catch (error) {
  self.postMessage({
    type: 'ERROR',
    error: error instanceof Error ? error.message : String(error)
  })
}

八、终止 Worker 和取消任务

8.1 主线程终止

worker.terminate()

terminate() 会立即停止 Worker,不会等待正在执行的任务完成。因此,如果任务需要保存结果,应先设计完成或取消协议。

8.2 Worker 自身关闭

self.close()

self.close() 只会关闭当前 Worker。

8.3 使用消息取消任务

长时间任务可以设计取消消息:

// 主线程
worker.postMessage({
  type: 'START',
  id: 'task-1'
})

worker.postMessage({
  type: 'CANCEL',
  id: 'task-1'
})
// Worker
const cancelledTasks = new Set()

self.onmessage = ({ data }) => {
  if (data.type === 'CANCEL') {
    cancelledTasks.add(data.id)
    return
  }

  if (data.type === 'START') {
    runTask(data.id)
  }
}

async function runTask(id) {
  for (let index = 0; index < 1000; index += 1) {
    if (cancelledTasks.has(id)) {
      self.postMessage({
        type: 'CANCELLED',
        id
      })
      cancelledTasks.delete(id)
      return
    }

    // 分批执行,让 Worker 有机会处理取消消息
    await Promise.resolve()
  }

  self.postMessage({
    type: 'DONE',
    id
  })
}

如果 Worker 正在执行一个完全同步且持续很久的循环,它在循环结束前无法处理新的消息。因此,取消机制需要把任务拆成多个阶段,或者直接终止 Worker。

九、加载其他脚本

Classic Worker 可以使用 importScripts()

importScripts('./math.js')
importScripts('./format.js', './validate.js')

importScripts() 会同步加载并执行脚本,只适用于 Classic Worker。

Module Worker 不应使用 importScripts(),而应使用 ES Module:

import { sum } from './math.js'

使用多个脚本时,应注意路径相对于 Worker 脚本或模块解析位置,而不是简单相对于当前 HTML 文件。

十、结构化克隆与 Transferable

10.1 结构化克隆

postMessage() 默认使用结构化克隆算法,而不是简单的字符串序列化:

worker.postMessage({
  name: 'demo',
  values: [1, 2, 3],
  createdAt: new Date()
})

接收方得到的是数据副本,修改副本不会影响发送方的普通对象:

// 主线程
const data = { count: 1 }
worker.postMessage(data)
data.count = 2

Worker 收到的是发送时克隆的数据,不会自动跟随主线程对象变化。

不能直接发送函数和 DOM 节点:

// 通常无法被结构化克隆
worker.postMessage(document.querySelector('#app'))

10.2 Transferable

对于大型二进制数据,可以转移对象所有权,避免复制:

const buffer = new ArrayBuffer(1024 * 1024)

worker.postMessage(
  { type: 'PROCESS_BUFFER', buffer },
  [buffer]
)

转移后,主线程不再拥有 buffer 的使用权:

console.log(buffer.byteLength) // 0,通常已被分离

Worker 中接收:

self.onmessage = ({ data }) => {
  const { buffer } = data
  const view = new Uint8Array(buffer)

  self.postMessage({
    type: 'DONE',
    length: view.length
  })
}

常见的 Transferable 包括:

  • ArrayBuffer
  • MessagePort
  • ImageBitmap
  • 部分浏览器支持的流对象。

10.3 TypedArray 的注意事项

TypedArray 本身通常是一个视图,真正的底层数据位于其 buffer 中:

const view = new Uint8Array(10)

worker.postMessage(view, [view.buffer])

转移 view.buffer 后,主线程不能继续使用这个 TypedArray 对应的底层缓冲区。

10.4 SharedArrayBuffer

SharedArrayBuffer 可以让多个执行上下文共享内存,但需要满足跨源隔离条件,并且通常还要配合 Atomics 处理并发同步。共享内存使用不当可能产生数据竞争,不应在没有明确并发模型时随意使用。

十一、模块 Worker

现代项目更推荐模块 Worker:

主线程

const worker = new Worker(
  new URL('./math.worker.js', import.meta.url),
  { type: 'module' }
)

worker.postMessage({
  numbers: [10, 20, 30]
})

worker.onmessage = ({ data }) => {
  console.log('计算结果:', data)
}

math.worker.js

import { sum } from './math.js'

self.onmessage = ({ data }) => {
  self.postMessage(sum(data.numbers))
}

math.js

export function sum(numbers) {
  return numbers.reduce((total, value) => total + value, 0)
}

模块 Worker 的特点:

  • 可以使用静态 import
  • 默认按照模块规则解析依赖;
  • 使用严格模式;
  • 不能使用 importScripts()
  • 构建工具通常可以自动处理依赖和产物路径。

十二、在 Vite、Vue 和 Nuxt 中使用

12.1 Vite 和 Webpack 5 推荐写法

现代 Vite 和 Webpack 5 通常不需要 worker-loader

const worker = new Worker(
  new URL('./workers/task.worker.js', import.meta.url),
  { type: 'module' }
)

Vite 也支持:

import TaskWorker from './workers/task.worker.js?worker'

const worker = new TaskWorker()

不建议在新项目中直接复制旧版 worker-loader 配置,因为它属于较早期的 Webpack 方案。

12.2 Vue 组件中使用

Worker 应在组件挂载后创建,并在组件销毁时终止:

<script setup>
import { onBeforeUnmount, onMounted } from 'vue'

let worker

onMounted(() => {
  worker = new Worker(
    new URL('./workers/task.worker.js', import.meta.url),
    { type: 'module' }
  )

  worker.onmessage = ({ data }) => {
    console.log('Worker 结果:', data)
  }

  worker.postMessage({ type: 'START' })
})

onBeforeUnmount(() => {
  worker?.terminate()
})
</script>

12.3 Nuxt 中使用

Nuxt 运行在 SSR 环境时,服务端没有 windowWorker。应在客户端生命周期中创建:

<script setup>
import { onBeforeUnmount, onMounted } from 'vue'

let worker

onMounted(() => {
  worker = new Worker(
    new URL('../workers/task.worker.js', import.meta.url),
    { type: 'module' }
  )
})

onBeforeUnmount(() => {
  worker?.terminate()
})
</script>

不要在模块顶层直接执行:

// SSR 阶段可能报错
const worker = new Worker('./worker.js')

12.4 使用 Worker 的组件注意事项

  • Worker 不应在每次组件更新时重复创建;
  • 组件卸载时应终止 Worker;
  • 监听器和消息端口应及时清理;
  • 需要处理 Worker 加载失败和运行错误;
  • 不要把不可序列化的 Vue 响应式对象直接传给 Worker;
  • 只传递必要的数据,避免频繁复制大型对象。

十三、适用场景与不适用场景

13.1 适用场景

  • 大量数据排序、过滤和统计;
  • JSON、CSV、Markdown 等复杂文本解析;
  • 加密、解密、压缩和哈希;
  • 图像像素计算;
  • 音视频数据处理;
  • 路径规划和复杂算法;
  • 使用 OffscreenCanvas 的图像或 Canvas 处理;
  • 对大文件进行分片计算。

13.2 不适用场景

以下任务通常不需要 Worker:

  • 简单的加减乘除;
  • 少量数组操作;
  • 普通 DOM 更新;
  • 很短的请求处理;
  • 只需要读取页面状态的轻量逻辑;
  • 需要频繁与 DOM 交互的任务。

Worker 无法直接操作 DOM。如果任务需要每一步都读取或修改 DOM,通信开销可能比计算本身更大。

13.3 图像和 Canvas

Worker 不能直接操作普通页面 DOM,也不能直接使用普通 Canvas。需要使用:

  • OffscreenCanvas
  • ImageBitmap
  • 或者只在 Worker 中计算像素数据,再传回主线程绘制。

浏览器对这些 API 的支持和性能存在差异,应在目标设备上测试。

十四、Web Worker、Shared Worker 与 Service Worker

类型主要用途生命周期通信范围
Dedicated Worker页面专属计算通常由创建它的页面管理创建者与 Worker
Shared Worker多个页面共享同一个 Worker由浏览器管理连接同源多个页面
Service Worker缓存、离线、Push、后台事件事件驱动,可被终止和唤醒作用域内多个页面

它们不能互相替代:

  • 需要把计算移出页面主线程时使用 Dedicated Worker;
  • 需要多个页面共享计算服务时考虑 Shared Worker;
  • 需要拦截网络请求或实现离线能力时使用 Service Worker。

十五、性能和安全注意事项

15.1 控制 Worker 数量

Worker 不是越多越好。过多 Worker 会造成:

  • CPU 竞争;
  • 内存占用增加;
  • 调度成本增加;
  • 页面电量消耗增加。

可以根据任务量复用一个 Worker,或实现有限大小的 Worker 池。

15.2 减少消息频率和数据大小

不要在高频事件中把大量对象不断发送给 Worker:

// 滚动事件中频繁发送大对象,可能造成性能问题
window.addEventListener('scroll', () => {
  worker.postMessage(largeObject)
})

可以使用节流、批处理、Transferable 或只发送必要字段。

15.3 处理错误和超时

任务可能因为脚本错误、数据格式错误或浏览器资源不足而失败。建议:

  • 给每个任务分配 id
  • 约定 RESULTERRORCANCELLED 等消息类型;
  • 设置任务超时;
  • 失败后决定重试、降级到主线程还是提示用户。

15.4 注意数据安全

Worker 脚本和页面脚本一样,需要防范供应链风险和 XSS 风险。不要从不可信来源加载 Worker 脚本,不要把敏感数据通过不受信任的消息通道发送出去。

十六、总结

  1. Web Worker 用于创建独立 JavaScript 执行上下文,减少计算对页面主线程的阻塞;
  2. Worker 通常由浏览器安排在独立线程执行,但不是永久运行的后台进程;
  3. Worker 不能访问页面 DOM、windowdocument
  4. 页面与 Worker 通过 postMessage() 异步通信;
  5. 普通数据默认使用结构化克隆,不能简单理解为 JSON 字符串序列化;
  6. 大型二进制数据可以使用 Transferable 转移所有权,减少复制开销;
  7. importScripts() 只适用于 Classic Worker,Module Worker 应使用 import
  8. 主线程应正确监听 errormessageerror
  9. 不再使用 Worker 时,应根据任务状态调用 terminate()self.close()
  10. Vite、Webpack 5、Vue 和 Nuxt 项目通常不需要旧版 worker-loader
  11. Worker 适合计算密集型任务,不适合频繁操作 DOM 的任务;
  12. 使用前应评估 Worker 创建、消息传输、数据复制和内存占用成本。

参考资料

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

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

按分类浏览

10 COLLECTIONS