Web Worker 多线程实战
大约 18 分钟约 5387 字
Web Worker 多线程实战
简介
JavaScript 是单线程语言,所有代码运行在主线程上。当执行计算密集型任务(如图片处理、大数据分析、加密运算)时,主线程会被阻塞,导致页面卡顿、无响应。Web Worker 提供了一种在后台线程中运行脚本的机制,使主线程保持流畅,是前端性能优化的重要手段。本文系统讲解 Dedicated Worker、Shared Worker、Service Worker 的使用场景和实战技巧。
特点
Worker 类型对比
三种 Worker 对比表
| 特性 | Dedicated Worker | Shared Worker | Service Worker |
|---|---|---|---|
| 绑定关系 | 单个页面 | 多个页面共享 | 全局代理 |
| 生命周期 | 随页面创建销毁 | 最后一个连接关闭时销毁 | 独立生命周期 |
| 通信方式 | postMessage | MessagePort | postMessage / FetchEvent |
| 使用场景 | 计算密集任务 | 跨标签通信 | 离线缓存、推送 |
| DOM 访问 | 不可以 | 不可以 | 不可以 |
| 文件缓存 | 不可以 | 不可以 | 可以 |
Dedicated Worker 基础
创建和使用 Worker
// worker/fibonacci.worker.ts
// 斐波那契数列计算 - 经典计算密集型任务
interface WorkerMessage {
type: 'fibonacci' | 'prime' | 'sort'
payload: any
}
interface WorkerResponse {
type: string
result: any
duration: number
}
self.onmessage = (event: MessageEvent<WorkerMessage>) => {
const { type, payload } = event.data
const startTime = performance.now()
let result: any
switch (type) {
case 'fibonacci':
result = fibonacci(payload.n)
break
case 'prime':
result = findPrimes(payload.max)
break
case 'sort':
result = heavySort(payload.array)
break
default:
result = null
}
const duration = performance.now() - startTime
const response: WorkerResponse = {
type,
result,
duration
}
self.postMessage(response)
}
// 递归斐波那契(故意不用动态规划以模拟耗时操作)
function fibonacci(n: number): number {
if (n <= 1) return n
return fibonacci(n - 1) + fibonacci(n - 2)
}
// 查找素数
function findPrimes(max: number): number[] {
const primes: number[] = []
for (let i = 2; i <= max; i++) {
if (isPrime(i)) primes.push(i)
}
return primes
}
function isPrime(num: number): boolean {
if (num < 2) return false
for (let i = 2; i <= Math.sqrt(num); i++) {
if (num % i === 0) return false
}
return true
}
// 大数组排序
function heavySort(array: number[]): number[] {
return [...array].sort((a, b) => a - b)
}主线程 Worker 管理
// utils/worker-manager.ts
export class WorkerManager {
private worker: Worker | null = null
private taskId = 0
private pendingTasks = new Map<number, {
resolve: (value: any) => void
reject: (reason: any) => void
}>()
constructor(private workerUrl: string) {}
// 初始化 Worker
init(): void {
this.worker = new Worker(this.workerUrl, { type: 'module' })
this.worker.onmessage = (event) => {
const { type, result, duration, taskId } = event.data
const pending = this.pendingTasks.get(taskId)
if (pending) {
pending.resolve({ type, result, duration })
this.pendingTasks.delete(taskId)
}
}
this.worker.onerror = (error) => {
console.error('Worker 错误:', error.message)
// 拒绝所有待处理的任务
for (const [id, pending] of this.pendingTasks) {
pending.reject(new Error(`Worker error: ${error.message}`))
}
this.pendingTasks.clear()
}
}
// 发送任务并等待结果
execute<T = any>(type: string, payload: any): Promise<{
type: string
result: T
duration: number
}> {
if (!this.worker) {
this.init()
}
const currentTaskId = ++this.taskId
return new Promise((resolve, reject) => {
this.pendingTasks.set(currentTaskId, { resolve, reject })
this.worker!.postMessage({ type, payload, taskId: currentTaskId })
})
}
// 终止 Worker
terminate(): void {
if (this.worker) {
this.worker.terminate()
this.worker = null
this.pendingTasks.clear()
}
}
}
// 使用示例
const workerManager = new WorkerManager('/workers/fibonacci.worker.js')
async function runComputeTask() {
try {
const result = await workerManager.execute<number>('fibonacci', { n: 42 })
console.log(`计算结果: ${result.result}, 耗时: ${result.duration}ms`)
} catch (error) {
console.error('计算失败:', error)
}
}通信模式
postMessage 基础通信
// 主线程
const worker = new Worker('/worker.js')
// 发送简单消息
worker.postMessage({ command: 'start', data: [1, 2, 3] })
// 接收消息
worker.onmessage = (event) => {
console.log('Worker 返回:', event.data)
}
// worker.js
self.onmessage = (event) => {
const { command, data } = event.data
const result = processData(data)
self.postMessage({ command, result })
}Transferable Objects 零拷贝传输
// Transferable 传输 - ArrayBuffer 可零拷贝传递
// utils/transferable-demo.ts
// 主线程:发送大型 ArrayBuffer 到 Worker
function sendLargeData() {
const size = 1024 * 1024 * 100 // 100MB
const buffer = new ArrayBuffer(size)
const view = new Float64Array(buffer)
// 填充数据
for (let i = 0; i < view.length; i++) {
view[i] = Math.random()
}
const worker = new Worker('/workers/process.worker.js')
console.log('传输前 buffer 大小:', buffer.byteLength) // 100MB
// 第二个参数指定 transferable 对象列表
// 传输后主线程将无法访问该 buffer(所有权转移)
worker.postMessage({ buffer }, [buffer])
console.log('传输后 buffer 大小:', buffer.byteLength) // 0,所有权已转移
worker.onmessage = (event) => {
// Worker 处理完成后可以把 buffer 传回来
const resultBuffer = event.data.buffer
console.log('处理完成,buffer 大小:', resultBuffer.byteLength)
}
}
// worker.js - 接收并处理大型数据
self.onmessage = (event) => {
const { buffer } = event.data
const view = new Float64Array(buffer)
// 在 Worker 中处理数据(不阻塞主线程)
const sum = view.reduce((acc, val) => acc + val, 0)
const avg = sum / view.length
// 处理完后将 buffer 传回主线程
self.postMessage({ buffer, avg }, [buffer])
}结构化克隆与序列化
// 结构化克隆算法支持的数据类型
const worker = new Worker('/worker.js')
// 可以传递的复杂类型
const complexData = {
// 基本类型
number: 42,
string: 'hello',
boolean: true,
// 复杂类型
date: new Date(),
regex: /pattern/g,
array: [1, 2, 3],
object: { nested: { deep: true } },
// 类型化数组
int32Array: new Int32Array([1, 2, 3]),
float64Array: new Float64Array([1.1, 2.2]),
// Map 和 Set
map: new Map([['key', 'value']]),
set: new Set([1, 2, 3]),
// Blob
blob: new Blob(['content'], { type: 'text/plain' })
}
// 注意:不能传递 Function、DOM 元素、Symbol
worker.postMessage(complexData)Worker Pool 实现
固定大小线程池
// utils/worker-pool.ts
interface PoolTask {
id: number
resolve: (value: any) => void
reject: (reason: any) => void
message: any
transfer?: Transferable[]
}
interface PoolWorker {
worker: Worker
busy: boolean
currentTask?: number
}
export class WorkerPool {
private workers: PoolWorker[] = []
private taskQueue: PoolTask[] = []
private taskId = 0
constructor(
private workerUrl: string,
private size: number = navigator.hardwareConcurrency || 4
) {
this.init()
}
private init(): void {
for (let i = 0; i < this.size; i++) {
const worker = new Worker(this.workerUrl, { type: 'module' })
const poolWorker: PoolWorker = { worker, busy: false }
worker.onmessage = (event) => {
const { taskId, result } = event.data
poolWorker.busy = false
poolWorker.currentTask = undefined
// 处理下一个排队任务
this.processQueue()
// 注意:resolve 在 postTask 中绑定到了 taskId
}
worker.onerror = (error) => {
poolWorker.busy = false
poolWorker.currentTask = undefined
this.processQueue()
}
this.workers.push(poolWorker)
}
}
// 提交任务到线程池
postTask<T = any>(
message: any,
transfer?: Transferable[]
): Promise<T> {
return new Promise((resolve, reject) => {
const taskId = ++this.taskId
const task: PoolTask = {
id: taskId,
resolve,
reject,
message: { ...message, taskId },
transfer
}
const availableWorker = this.workers.find(w => !w.busy)
if (availableWorker) {
this.executeTask(availableWorker, task)
} else {
// 没有空闲 Worker,加入队列
this.taskQueue.push(task)
}
})
}
private executeTask(poolWorker: PoolWorker, task: PoolTask): void {
poolWorker.busy = true
poolWorker.currentTask = task.id
const handler = (event: MessageEvent) => {
if (event.data.taskId === task.id) {
poolWorker.worker.removeEventListener('message', handler)
task.resolve(event.data.result)
}
}
const errorHandler = (event: ErrorEvent) => {
poolWorker.worker.removeEventListener('error', errorHandler)
task.reject(new Error(event.message))
}
poolWorker.worker.addEventListener('message', handler)
poolWorker.worker.addEventListener('error', errorHandler)
if (task.transfer) {
poolWorker.worker.postMessage(task.message, task.transfer)
} else {
poolWorker.worker.postMessage(task.message)
}
}
private processQueue(): void {
if (this.taskQueue.length === 0) return
const availableWorker = this.workers.find(w => !w.busy)
if (!availableWorker) return
const task = this.taskQueue.shift()!
this.executeTask(availableWorker, task)
}
// 终止所有 Worker
terminate(): void {
this.workers.forEach(({ worker }) => worker.terminate())
this.workers = []
this.taskQueue = []
}
// 获取线程池状态
getStatus(): {
total: number
busy: number
idle: number
queued: number
} {
const busy = this.workers.filter(w => w.busy).length
return {
total: this.workers.length,
busy,
idle: this.workers.length - busy,
queued: this.taskQueue.length
}
}
}Worker Pool 使用示例
// 使用线程池并行处理大数据
const pool = new WorkerPool('/workers/data.worker.js', 4)
async function processLargeDataset(data: number[][]): Promise<number[]> {
// 将数据分片,分发给不同的 Worker 并行处理
const promises = data.map(chunk =>
pool.postTask<number[]>({
type: 'analyze',
data: chunk
})
)
const results = await Promise.all(promises)
return results.flat()
}
// 示例:生成大数据集并处理
function runParallelAnalysis() {
const totalSize = 1_000_000
const chunkSize = 250_000
const chunks: number[][] = []
for (let i = 0; i < 4; i++) {
const chunk: number[] = []
for (let j = 0; j < chunkSize; j++) {
chunk.push(Math.random() * 1000)
}
chunks.push(chunk)
}
console.time('parallel')
processLargeDataset(chunks).then(result => {
console.timeEnd('parallel')
console.log('处理完成,结果数量:', result.length)
console.log('线程池状态:', pool.getStatus())
})
}SharedArrayBuffer 与 Atomics
共享内存多线程
// shared-memory.ts
// 注意:使用 SharedArrayBuffer 需要设置特定的 HTTP 头
// Cross-Origin-Opener-Policy: same-origin
// Cross-Origin-Embedder-Policy: require-corp
// 主线程:创建共享内存
function createSharedMemory() {
const bufferSize = 1024
const sharedBuffer = new SharedArrayBuffer(
Int32Array.BYTES_PER_ELEMENT * bufferSize
)
const sharedArray = new Int32Array(sharedBuffer)
// 初始化数据
for (let i = 0; i < bufferSize; i++) {
sharedArray[i] = i * 2
}
// 创建多个 Worker 共享同一块内存
const workerCount = 4
const workers: Worker[] = []
for (let i = 0; i < workerCount; i++) {
const worker = new Worker('/workers/shared.worker.js')
worker.postMessage({
sharedBuffer,
workerId: i,
startIdx: (bufferSize / workerCount) * i,
endIdx: (bufferSize / workerCount) * (i + 1)
})
workers.push(worker)
}
// 等待所有 Worker 完成
let completed = 0
workers.forEach(worker => {
worker.onmessage = () => {
completed++
if (completed === workerCount) {
console.log('所有 Worker 处理完成')
console.log('共享内存前10个值:', sharedArray.slice(0, 10))
}
}
})
}Worker 端使用 Atomics
// workers/shared.worker.ts
interface SharedWorkerMessage {
sharedBuffer: SharedArrayBuffer
workerId: number
startIdx: number
endIdx: number
}
self.onmessage = (event: MessageEvent<SharedWorkerMessage>) => {
const { sharedBuffer, workerId, startIdx, endIdx } = event.data
const sharedArray = new Int32Array(sharedBuffer)
// 使用 Atomics 安全地操作共享内存
for (let i = startIdx; i < endIdx; i++) {
// 原子加操作
Atomics.add(sharedArray, i, 1)
// 原子比较并交换
const expected = i * 2 + 1
Atomics.compareExchange(sharedArray, i, expected, i * 3)
// 原子存储
Atomics.store(sharedArray, i, sharedArray[i] * 2)
}
// 通知主线程完成
self.postMessage({ workerId, completed: true })
// 使用 Atomics.wait/notify 进行线程同步
// Atomics.wait(sharedArray, index, expectedValue, timeout)
// Atomics.notify(sharedArray, index, count)
}OffscreenCanvas 在 Worker 中渲染
OffscreenCanvas 基础
// 主线程:将 Canvas 控制权转交给 Worker
function setupOffscreenCanvas() {
const canvas = document.getElementById('myCanvas') as HTMLCanvasElement
const offscreen = canvas.transferControlToOffscreen()
const worker = new Worker('/workers/canvas.worker.js')
// 传输 OffscreenCanvas 到 Worker
worker.postMessage({ type: 'init', canvas: offscreen }, [offscreen])
// 发送绘制指令
worker.postMessage({
type: 'draw',
shape: 'circle',
x: 200,
y: 200,
radius: 50,
color: '#ff6600'
})
// 动画控制
worker.postMessage({ type: 'animate', enable: true })
// 响应来自 Worker 的事件
worker.onmessage = (event) => {
if (event.data.type === 'fps') {
console.log('当前 FPS:', event.data.value)
}
}
}Worker 端 Canvas 渲染
// workers/canvas.worker.ts
let canvas: OffscreenCanvas | null = null
let ctx: OffscreenCanvasRenderingContext2D | null = null
let animationId = 0
let isAnimating = false
// 粒子系统
interface Particle {
x: number
y: number
vx: number
vy: number
radius: number
color: string
life: number
maxLife: number
}
const particles: Particle[] = []
self.onmessage = (event) => {
const { type, ...data } = event.data
switch (type) {
case 'init':
canvas = data.canvas as OffscreenCanvas
canvas.width = 800
canvas.height = 600
ctx = canvas.getContext('2d')!
break
case 'draw':
if (ctx) {
drawShape(data)
}
break
case 'animate':
isAnimating = data.enable
if (isAnimating) {
startAnimation()
} else {
cancelAnimationFrame(animationId)
}
break
case 'addParticles':
addParticles(data.count || 100)
break
}
}
function drawShape(params: any): void {
if (!ctx) return
ctx.clearRect(0, 0, 800, 600)
switch (params.shape) {
case 'circle':
ctx.beginPath()
ctx.arc(params.x, params.y, params.radius, 0, Math.PI * 2)
ctx.fillStyle = params.color
ctx.fill()
ctx.closePath()
break
case 'rect':
ctx.fillStyle = params.color
ctx.fillRect(params.x, params.y, params.width, params.height)
break
}
}
function addParticles(count: number): void {
for (let i = 0; i < count; i++) {
particles.push({
x: 400,
y: 300,
vx: (Math.random() - 0.5) * 4,
vy: (Math.random() - 0.5) * 4,
radius: Math.random() * 3 + 1,
color: `hsl(${Math.random() * 360}, 80%, 60%)`,
life: 0,
maxLife: Math.random() * 100 + 50
})
}
}
let lastTime = 0
let frameCount = 0
let fps = 0
function startAnimation(): void {
function animate(time: number) {
if (!ctx || !canvas) return
// 计算 FPS
frameCount++
if (time - lastTime >= 1000) {
fps = frameCount
frameCount = 0
lastTime = time
self.postMessage({ type: 'fps', value: fps })
}
// 清除画布
ctx.fillStyle = 'rgba(0, 0, 0, 0.1)'
ctx.fillRect(0, 0, canvas.width, canvas.height)
// 更新和绘制粒子
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i]
p.x += p.vx
p.y += p.vy
p.life++
if (p.life >= p.maxLife) {
particles.splice(i, 1)
continue
}
const alpha = 1 - p.life / p.maxLife
ctx.beginPath()
ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2)
ctx.fillStyle = p.color.replace(')', `, ${alpha})`).replace('hsl', 'hsla')
ctx.fill()
ctx.closePath()
}
if (isAnimating) {
animationId = requestAnimationFrame(animate)
}
}
animationId = requestAnimationFrame(animate)
}图片处理实战
Worker 中处理图片
// workers/image-processor.worker.ts
// 图片灰度化、模糊、边缘检测等处理
interface ImageTask {
type: 'grayscale' | 'blur' | 'sharpen' | 'edge' | 'resize'
imageData: ImageData
params?: any
}
self.onmessage = (event: MessageEvent<ImageTask>) => {
const { type, imageData, params } = event.data
const startTime = performance.now()
let result: ImageData
switch (type) {
case 'grayscale':
result = applyGrayscale(imageData)
break
case 'blur':
result = applyBoxBlur(imageData, params?.radius || 3)
break
case 'sharpen':
result = applySharpen(imageData)
break
case 'edge':
result = applySobelEdge(imageData)
break
default:
result = imageData
}
const duration = performance.now() - startTime
self.postMessage({ imageData: result, duration })
}
function applyGrayscale(imageData: ImageData): ImageData {
const data = imageData.data
const output = new ImageData(
new Uint8ClampedArray(data),
imageData.width,
imageData.height
)
for (let i = 0; i < data.length; i += 4) {
const r = data[i]
const g = data[i + 1]
const b = data[i + 2]
// 加权灰度公式(符合人眼感知)
const gray = 0.299 * r + 0.587 * g + 0.114 * b
output.data[i] = gray
output.data[i + 1] = gray
output.data[i + 2] = gray
output.data[i + 3] = data[i + 3] // alpha 不变
}
return output
}
function applyBoxBlur(imageData: ImageData, radius: number): ImageData {
const { width, height, data } = imageData
const output = new ImageData(
new Uint8ClampedArray(data),
width,
height
)
const kernelSize = radius * 2 + 1
const kernelArea = kernelSize * kernelSize
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
let rSum = 0, gSum = 0, bSum = 0
for (let ky = -radius; ky <= radius; ky++) {
for (let kx = -radius; kx <= radius; kx++) {
const px = Math.min(width - 1, Math.max(0, x + kx))
const py = Math.min(height - 1, Math.max(0, y + ky))
const idx = (py * width + px) * 4
rSum += data[idx]
gSum += data[idx + 1]
bSum += data[idx + 2]
}
}
const idx = (y * width + x) * 4
output.data[idx] = rSum / kernelArea
output.data[idx + 1] = gSum / kernelArea
output.data[idx + 2] = bSum / kernelArea
output.data[idx + 3] = data[idx + 3]
}
}
return output
}
function applySobelEdge(imageData: ImageData): ImageData {
const { width, height, data } = imageData
const output = new ImageData(width, height)
// Sobel 算子
const sobelX = [-1, 0, 1, -2, 0, 2, -1, 0, 1]
const sobelY = [-1, -2, -1, 0, 0, 0, 1, 2, 1]
for (let y = 1; y < height - 1; y++) {
for (let x = 1; x < width - 1; x++) {
let gx = 0, gy = 0
for (let ky = -1; ky <= 1; ky++) {
for (let kx = -1; kx <= 1; kx++) {
const idx = ((y + ky) * width + (x + kx)) * 4
const gray = data[idx] * 0.299 + data[idx + 1] * 0.587 + data[idx + 2] * 0.114
const ki = (ky + 1) * 3 + (kx + 1)
gx += gray * sobelX[ki]
gy += gray * sobelY[ki]
}
}
const magnitude = Math.min(255, Math.sqrt(gx * gx + gy * gy))
const idx = (y * width + x) * 4
output.data[idx] = magnitude
output.data[idx + 1] = magnitude
output.data[idx + 2] = magnitude
output.data[idx + 3] = 255
}
}
return output
}图片处理使用示例
// 主线程:图片处理流程
class ImageProcessor {
private worker: Worker
constructor() {
this.worker = new Worker('/workers/image-processor.worker.js')
}
async processImage(
imageElement: HTMLImageElement,
operation: 'grayscale' | 'blur' | 'edge',
params?: any
): Promise<string> {
// 从图片元素创建 ImageData
const canvas = document.createElement('canvas')
canvas.width = imageElement.naturalWidth
canvas.height = imageElement.naturalHeight
const ctx = canvas.getContext('2d')!
ctx.drawImage(imageElement, 0, 0)
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
// 发送到 Worker 处理
return new Promise((resolve, reject) => {
this.worker.onmessage = (event) => {
const { imageData: resultData, duration } = event.data
console.log(`图片处理耗时: ${duration.toFixed(2)}ms`)
// 将处理后的 ImageData 绘制回 Canvas
ctx.putImageData(resultData, 0, 0)
resolve(canvas.toDataURL('image/png'))
}
this.worker.onerror = reject
this.worker.postMessage({ type: operation, imageData, params })
})
}
terminate(): void {
this.worker.terminate()
}
}
// 使用示例
async function processUserImage() {
const img = document.getElementById('source') as HTMLImageElement
const processor = new ImageProcessor()
try {
// 灰度化
const grayscaleUrl = await processor.processImage(img, 'grayscale')
document.getElementById('grayscale')!.setAttribute('src', grayscaleUrl)
// 边缘检测
const edgeUrl = await processor.processImage(img, 'edge')
document.getElementById('edge')!.setAttribute('src', edgeUrl)
// 模糊
const blurUrl = await processor.processImage(img, 'blur', { radius: 5 })
document.getElementById('blur')!.setAttribute('src', blurUrl)
} finally {
processor.terminate()
}
}Shared Worker
跨标签页通信
// workers/shared-counter.worker.ts
// 多标签页共享的计数器
const connections: MessagePort[] = []
let sharedCounter = 0
self.onconnect = (event: Event) => {
const port = (event as MessageEvent).ports[0]
connections.push(port)
port.onmessage = (e: MessageEvent) => {
const { type, value } = e.data
switch (type) {
case 'increment':
sharedCounter += value || 1
broadcast({ type: 'update', value: sharedCounter })
break
case 'decrement':
sharedCounter -= value || 1
broadcast({ type: 'update', value: sharedCounter })
break
case 'get':
port.postMessage({ type: 'update', value: sharedCounter })
break
case 'reset':
sharedCounter = 0
broadcast({ type: 'update', value: sharedCounter })
break
}
}
// 发送当前值给新连接
port.postMessage({ type: 'update', value: sharedCounter })
port.start()
}
function broadcast(message: any): void {
connections.forEach(port => {
try {
port.postMessage(message)
} catch {
// 连接已断开,移除
const idx = connections.indexOf(port)
if (idx > -1) connections.splice(idx, 1)
}
})
}使用 Shared Worker
// 主线程:连接 Shared Worker
class SharedCounterClient {
private port: MessagePort
constructor() {
const worker = new SharedWorker('/workers/shared-counter.worker.js')
this.port = worker.port
this.port.start()
this.port.onmessage = (event) => {
const { type, value } = event.data
if (type === 'update') {
console.log('共享计数器:', value)
document.getElementById('counter')!.textContent = String(value)
}
}
}
increment(): void {
this.port.postMessage({ type: 'increment', value: 1 })
}
decrement(): void {
this.port.postMessage({ type: 'decrement', value: 1 })
}
reset(): void {
this.port.postMessage({ type: 'reset' })
}
}Service Worker 简介
Service Worker 注册与缓存策略
// sw-register.ts
// 注册 Service Worker
export async function registerServiceWorker(): Promise<void> {
if (!('serviceWorker' in navigator)) {
console.warn('当前浏览器不支持 Service Worker')
return
}
try {
const registration = await navigator.serviceWorker.register('/sw.js', {
scope: '/'
})
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing
if (newWorker) {
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'activated') {
console.log('新版本已激活,刷新页面获取最新内容')
}
})
}
})
console.log('Service Worker 注册成功')
} catch (error) {
console.error('Service Worker 注册失败:', error)
}
}Service Worker 缓存策略实现
// sw.js - Service Worker 缓存策略
const CACHE_NAME = 'app-cache-v1'
const STATIC_ASSETS = [
'/',
'/index.html',
'/styles/main.css',
'/scripts/main.js',
'/favicon.ico'
]
// 安装事件:预缓存静态资源
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => {
return cache.addAll(STATIC_ASSETS)
})
)
self.skipWaiting()
})
// 激活事件:清理旧缓存
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then(keys =>
Promise.all(
keys
.filter(key => key !== CACHE_NAME)
.map(key => caches.delete(key))
)
)
)
self.clients.claim()
})
// Fetch 事件:缓存策略
self.addEventListener('fetch', (event) => {
const { request } = event
const url = new URL(request.url)
// API 请求:网络优先
if (url.pathname.startsWith('/api/')) {
event.respondWith(networkFirst(request))
return
}
// 静态资源:缓存优先
if (isStaticAsset(url.pathname)) {
event.respondWith(cacheFirst(request))
return
}
// 其他请求:Stale-While-Revalidate
event.respondWith(staleWhileRevalidate(request))
})
async function cacheFirst(request: Request): Promise<Response> {
const cached = await caches.match(request)
if (cached) return cached
const response = await fetch(request)
const cache = await caches.open(CACHE_NAME)
cache.put(request, response.clone())
return response
}
async function networkFirst(request: Request): Promise<Response> {
try {
const response = await fetch(request)
const cache = await caches.open(CACHE_NAME)
cache.put(request, response.clone())
return response
} catch {
const cached = await caches.match(request)
if (cached) return cached
return new Response('离线模式', { status: 503 })
}
}
async function staleWhileRevalidate(request: Request): Promise<Response> {
const cache = await caches.open(CACHE_NAME)
const cached = await cache.match(request)
const fetchPromise = fetch(request).then(response => {
cache.put(request, response.clone())
return response
})
return cached || fetchPromise
}
function isStaticAsset(pathname: string): boolean {
return /\.(js|css|png|jpg|jpeg|gif|svg|woff2?|ttf|eot|ico)$/.test(pathname)
}错误处理与调试
Worker 错误处理
// utils/worker-error-handler.ts
export class SafeWorker {
private worker: Worker | null = null
private retries = 0
private maxRetries = 3
constructor(
private url: string,
private onError?: (error: Error) => void
) {}
start(): void {
this.worker = new Worker(this.url)
this.worker.onerror = (event: ErrorEvent) => {
console.error('Worker 运行错误:', {
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno
})
const error = new Error(
`Worker error at ${event.filename}:${event.lineno}:${event.colno} - ${event.message}`
)
this.onError?.(error)
// 自动重试
if (this.retries < this.maxRetries) {
this.retries++
console.log(`正在重试启动 Worker (${this.retries}/${this.maxRetries})`)
this.terminate()
setTimeout(() => this.start(), 1000 * this.retries)
}
}
}
postMessage(message: any, transfer?: Transferable[]): void {
if (!this.worker) throw new Error('Worker 未启动')
if (transfer) {
this.worker.postMessage(message, transfer)
} else {
this.worker.postMessage(message)
}
}
onMessage(handler: (event: MessageEvent) => void): void {
if (!this.worker) throw new Error('Worker 未启动')
this.worker.onmessage = handler
}
terminate(): void {
this.worker?.terminate()
this.worker = null
}
}
// 使用示例
const worker = new SafeWorker('/workers/heavy-task.worker.js', (error) => {
// 上报错误到监控系统
reportError(error)
})
worker.start()
worker.onMessage((event) => {
console.log('结果:', event.data)
})
worker.postMessage({ type: 'start' })Worker 调试技巧
// Worker 内部日志转发到主线程
// workers/debug-logger.ts
const originalConsole = { ...console }
// 重写 Worker 内部的 console 方法
(self as any).console = {
log: (...args: any[]) => {
originalConsole.log(...args)
self.postMessage({
__debug__: true,
level: 'log',
args: args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)),
timestamp: Date.now()
})
},
error: (...args: any[]) => {
originalConsole.error(...args)
self.postMessage({
__debug__: true,
level: 'error',
args: args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)),
timestamp: Date.now()
})
},
warn: (...args: any[]) => {
originalConsole.warn(...args)
self.postMessage({
__debug__: true,
level: 'warn',
args: args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)),
timestamp: Date.now()
})
}
}性能基准测试
Worker 与主线程性能对比
// utils/worker-benchmark.ts
interface BenchmarkResult {
name: string
mainThreadTime: number
workerTime: number
speedup: number
blocked: boolean
}
export class WorkerBenchmark {
private worker: Worker
constructor() {
this.worker = new Worker('/workers/benchmark.worker.js')
}
async runAll(): Promise<BenchmarkResult[]> {
const results: BenchmarkResult[] = []
// 测试 1:斐波那契数列
results.push(await this.runTest('fibonacci-40', { n: 40 }))
results.push(await this.runTest('fibonacci-45', { n: 45 }))
// 测试 2:大数组排序
results.push(await this.runTest('sort-100k', { size: 100000 }))
results.push(await this.runTest('sort-1m', { size: 1000000 }))
// 测试 3:矩阵运算
results.push(await this.runTest('matrix-100', { size: 100 }))
results.push(await this.runTest('matrix-500', { size: 500 }))
// 测试 4:JSON 解析
results.push(await this.runTest('json-parse', { size: 50000 }))
return results
}
private async runTest(
name: string,
params: any
): Promise<BenchmarkResult> {
// 主线程测试
const mainStart = performance.now()
this.runMainThread(name, params)
const mainTime = performance.now() - mainStart
// Worker 线程测试
const workerTime = await this.runWorkerTest(name, params)
return {
name,
mainThreadTime: mainTime,
workerTime,
speedup: mainTime / workerTime,
blocked: mainTime > 50 // 超过 50ms 视为阻塞
}
}
private runMainThread(name: string, params: any): void {
switch (name.split('-')[0]) {
case 'fibonacci':
this.fibonacci(params.n)
break
case 'sort': {
const arr = Array.from({ length: params.size }, () => Math.random())
arr.sort((a, b) => a - b)
break
}
case 'matrix':
this.matrixMultiply(params.size)
break
}
}
private runWorkerTest(name: string, params: any): Promise<number> {
return new Promise((resolve) => {
const start = performance.now()
this.worker.onmessage = () => {
resolve(performance.now() - start)
}
this.worker.postMessage({ name, params })
})
}
private fibonacci(n: number): number {
if (n <= 1) return n
return this.fibonacci(n - 1) + this.fibonacci(n - 2)
}
private matrixMultiply(size: number): number[][] {
const a = Array.from({ length: size }, () =>
Array.from({ length: size }, () => Math.random())
)
const b = Array.from({ length: size }, () =>
Array.from({ length: size }, () => Math.random())
)
const result: number[][] = []
for (let i = 0; i < size; i++) {
result[i] = []
for (let j = 0; j < size; j++) {
let sum = 0
for (let k = 0; k < size; k++) {
sum += a[i][k] * b[k][j]
}
result[i][j] = sum
}
}
return result
}
terminate(): void {
this.worker.terminate()
}
}优点
- 主线程不阻塞:计算密集型任务放到 Worker 中,页面保持流畅
- 充分利用多核 CPU:Worker Pool 可以充分利用多核处理器
- 安全隔离:Worker 无法直接操作 DOM,降低安全风险
- 离线能力:Service Worker 实现离线缓存和后台同步
- 跨标签页共享:Shared Worker 实现多标签页数据共享
缺点
- 通信开销:postMessage 需要序列化/反序列化,大对象传输开销大
- 内存占用:每个 Worker 是独立线程,有额外的内存开销
- 调试困难:Worker 内部的断点和日志需要特殊处理
- 不能操作 DOM:Worker 中无法直接访问 DOM API
- 兼容性问题:SharedArrayBuffer 需要 COOP/COEP 头支持
- 创建开销:频繁创建销毁 Worker 有性能损耗
性能注意事项
- 通信数据量控制:尽量减少 postMessage 传递的数据量,使用 Transferable 零拷贝传输
- Worker 复用:使用 Worker Pool 复用 Worker,避免频繁创建销毁
- 粒度控制:任务太小时通信开销可能大于计算开销,需权衡
- 内存管理:Worker 中的大对象要手动释放,避免内存泄漏
- SharedArrayBuffer 安全头:必须配置 COOP/COEP 响应头
- Worker 数量控制:建议 Worker 数量不超过
navigator.hardwareConcurrency - 消息合并:多次小消息可合并为一次大消息发送
总结
Web Worker 是前端处理计算密集型任务的利器。Dedicated Worker 适合单页面的后台计算,Shared Worker 适合跨标签页通信,Service Worker 适合离线缓存和网络代理。通过 Worker Pool 可以充分利用多核 CPU,配合 Transferable 和 SharedArrayBuffer 可以高效地进行数据传输。在实际项目中,图片处理、数据分析、加密运算等场景都是 Worker 的典型应用。
关键知识点
- Dedicated Worker 与主线程通过 postMessage 通信
- Transferable Objects 可以零拷贝转移 ArrayBuffer 等数据
- SharedArrayBuffer + Atomics 实现真正的多线程共享内存
- OffscreenCanvas 允许 Worker 独立渲染 Canvas
- Worker Pool 管理 Worker 的创建、复用和销毁
- Service Worker 可以拦截请求并实现缓存策略
- Shared Worker 支持多个页面共享同一个 Worker 实例
常见误区
- Worker 能加速所有操作 — 通信开销可能抵消并行收益
- Worker 可以操作 DOM — Worker 中无法访问 document 和 window
- postMessage 是零拷贝 — 默认是结构化克隆(深拷贝),需用 Transferable 实现零拷贝
- Worker 越多越好 — 每个 Worker 占用独立线程和内存,过多反而降低性能
- SharedArrayBuffer 随便用 — 需要服务器配置特定的安全头
- Service Worker 只能做缓存 — 还可以做推送通知、后台同步、定期同步等
进阶路线
- 入门:Dedicated Worker 基本使用、postMessage 通信
- 进阶:Worker Pool、Transferable Objects、错误处理
- 高级:SharedArrayBuffer + Atomics、OffscreenCanvas
- 专家:WebAssembly + Worker 高性能计算、COM streaming 模式
- 架构:Actor 模型、CSP(Communicating Sequential Processes)并发模型
适用场景
- 图片处理(滤镜、裁剪、压缩)
- 大数据分析和统计
- 加密/解密运算
- PDF/文档解析
- 音视频编解码
- 复杂动画和粒子系统(OffscreenCanvas)
- WebSocket 长连接管理
- 后台数据同步(Service Worker)
落地建议
- 评估是否真的需要 Worker:任务执行时间超过 50ms 才有使用 Worker 的必要
- 选择合适的 Worker 类型:单一计算用 Dedicated,跨页共享用 Shared,离线缓存用 Service
- 使用 Worker Pool 管理线程:避免频繁创建销毁,复用 Worker 实例
- 优先使用 Transferable:大数据传输时务必使用 Transferable 减少拷贝
- 做好错误处理:Worker 中未捕获的错误会导致 Worker 终止
- 配合构建工具:使用 Vite/Webpack 的 Worker 插件简化 Worker 文件打包
排错清单
复盘问题
- 为什么 Worker 中不能操作 DOM?这样设计的好处是什么?
- Transferable 和结构化克隆的本质区别是什么?各适用什么场景?
- SharedArrayBuffer 为什么需要 COOP/COEP 安全头?Spectre 漏洞与此有什么关系?
- Worker Pool 的大小应该设置为多少?为什么不是越大越好?
- Service Worker 的生命周期有哪几个阶段?每个阶段的意义是什么?
- 如何在不支持 Worker 的浏览器中优雅降级?
