Web Worker 详解
Category(分类): HTML Status: 未知
本文对原文进行了重新整理和勘误。重点说明 Web Worker 的执行模型、限制、通信方式、错误处理、数据传输和现代构建工具用法。原文图片地址已经失效,未保留失效图片链接。
目录
- 一、为什么需要 Web Worker
- 二、Web Worker 是什么
- 三、Web Worker 的限制
- 四、创建和使用 Dedicated Worker
- 五、Worker 线程内部代码
- 六、消息通信
- 七、错误处理
- 八、终止 Worker 和取消任务
- 九、加载其他脚本
- 十、结构化克隆与 Transferable
- 十一、模块 Worker
- 十二、在 Vite、Vue 和 Nuxt 中使用
- 十三、适用场景与不适用场景
- 十四、Web Worker、Shared Worker 与 Service 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 中没有页面的 window 和 document,不能直接执行:
// 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 环境时,服务端没有 window 和 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' }
)
})
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; - 约定
RESULT、ERROR、CANCELLED等消息类型; - 设置任务超时;
- 失败后决定重试、降级到主线程还是提示用户。
15.4 注意数据安全
Worker 脚本和页面脚本一样,需要防范供应链风险和 XSS 风险。不要从不可信来源加载 Worker 脚本,不要把敏感数据通过不受信任的消息通道发送出去。
十六、总结
- Web Worker 用于创建独立 JavaScript 执行上下文,减少计算对页面主线程的阻塞;
- Worker 通常由浏览器安排在独立线程执行,但不是永久运行的后台进程;
- Worker 不能访问页面 DOM、
window和document; - 页面与 Worker 通过
postMessage()异步通信; - 普通数据默认使用结构化克隆,不能简单理解为 JSON 字符串序列化;
- 大型二进制数据可以使用 Transferable 转移所有权,减少复制开销;
importScripts()只适用于 Classic Worker,Module Worker 应使用import;- 主线程应正确监听
error和messageerror; - 不再使用 Worker 时,应根据任务状态调用
terminate()或self.close(); - Vite、Webpack 5、Vue 和 Nuxt 项目通常不需要旧版
worker-loader; - Worker 适合计算密集型任务,不适合频繁操作 DOM 的任务;
- 使用前应评估 Worker 创建、消息传输、数据复制和内存占用成本。