跳到主要内容

Web Worker 性能优化

问题

如何使用 Web Worker 优化前端性能?Worker 有哪些类型?如何在主线程和 Worker 之间通信?

面试速答版

如何使用 Web Worker 优化前端性能? Worker 的价值在于把 CPU 密集型任务搬出主线程,让 UI 不卡:

  • 典型场景:大文件 hash(spark-md5)、JSON 解析超大数据、Excel 导出、图片处理、加密解密、复杂搜索/排序。
  • 用法:new Worker(new URL('./worker.ts', import.meta.url)) 创建,主线程和 Worker 通过 postMessage 通信。
  • 工程化:用 comlink 库把 Worker 方法当 Promise 调用,免去手写消息协议;Vite/Webpack 都原生支持 Worker 打包。
  • 别滥用:Worker 启动有开销(几 ms~几十 ms),小任务直接主线程更快;高频调用要复用 Worker 实例或建线程池。

Worker 有哪些类型? 三种各管一摊:

  • Dedicated Worker:最常用,单页面独占,关页面就销毁,处理 CPU 密集计算。
  • Shared Worker多个同源页面共享一个实例,可用于跨标签页通信、统一管理 WebSocket 连接;但 Safari 兼容性差,实际更多用 BroadcastChannel
  • Service Worker:完全不同的存在——是网络代理 + 缓存层,主要做 PWA、离线缓存、推送通知,生命周期独立于页面。

如何在主线程和 Worker 之间通信? 核心是 postMessage + onmessage 双向消息:

  • 数据走结构化克隆算法,会深拷贝(不是引用),传大数据有性能开销。
  • 大数据用 Transferable Objectsworker.postMessage(buffer, [buffer]),所有权转移而非拷贝,传 ArrayBuffer/MessagePort/ImageBitmap 几乎零成本,但原线程拿到的对象会失效。
  • 多任务并发时给每条消息带 id,回包用 id 匹配——这就是 comlink 在做的事,封装成 Promise 调用更顺手。
  • 不能传 DOM、函数、window/document;Worker 里没有这些 API,但有 fetchIndexedDBcrypto

答案

JavaScript 是单线程的,复杂计算会阻塞 UI。Web Worker 允许在后台线程执行脚本,不阻塞主线程,是处理 CPU 密集型任务的利器。


Worker 类型对比

类型作用域生命周期主要用途
Dedicated Worker单页面随页面关闭CPU 密集计算
Shared Worker多页面所有连接关闭跨标签通信
Service Worker域级别独立于页面离线缓存、推送

Dedicated Worker

基础用法

// main.ts - 主线程
const worker = new Worker(new URL('./worker.ts', import.meta.url));

// 发送消息
worker.postMessage({ type: 'calculate', data: [1, 2, 3] });

// 接收消息
worker.onmessage = (e: MessageEvent) => {
console.log('Result:', e.data);
};

// 错误处理
worker.onerror = (error) => {
console.error('Worker error:', error);
};

// 终止 Worker
worker.terminate();
// worker.ts - Worker 线程
self.onmessage = (e: MessageEvent) => {
const { type, data } = e.data;

if (type === 'calculate') {
const result = heavyCalculation(data);
self.postMessage(result);
}
};

function heavyCalculation(data: number[]): number {
// 复杂计算
return data.reduce((sum, n) => sum + n, 0);
}

封装 Promise Worker

// worker-wrapper.ts
type WorkerTask<T, R> = {
resolve: (value: R) => void;
reject: (error: Error) => void;
};

class PromiseWorker<T, R> {
private worker: Worker;
private taskId = 0;
private pending = new Map<number, WorkerTask<T, R>>();

constructor(workerUrl: URL) {
this.worker = new Worker(workerUrl);

this.worker.onmessage = (e: MessageEvent) => {
const { id, result, error } = e.data;
const task = this.pending.get(id);

if (task) {
if (error) {
task.reject(new Error(error));
} else {
task.resolve(result);
}
this.pending.delete(id);
}
};
}

postMessage(data: T): Promise<R> {
return new Promise((resolve, reject) => {
const id = ++this.taskId;
this.pending.set(id, { resolve, reject });
this.worker.postMessage({ id, data });
});
}

terminate() {
this.worker.terminate();
this.pending.forEach(task => {
task.reject(new Error('Worker terminated'));
});
this.pending.clear();
}
}

// worker.ts
self.onmessage = async (e: MessageEvent) => {
const { id, data } = e.data;

try {
const result = await processData(data);
self.postMessage({ id, result });
} catch (error) {
self.postMessage({ id, error: (error as Error).message });
}
};

// 使用
const worker = new PromiseWorker<number[], number>(
new URL('./worker.ts', import.meta.url)
);

const result = await worker.postMessage([1, 2, 3, 4, 5]);

Worker 线程池

避免创建过多 Worker,使用线程池管理。

class WorkerPool<T, R> {
private workers: Worker[] = [];
private taskQueue: Array<{
data: T;
resolve: (value: R) => void;
reject: (error: Error) => void;
}> = [];
private availableWorkers: Worker[] = [];

constructor(workerUrl: URL, poolSize = navigator.hardwareConcurrency || 4) {
for (let i = 0; i < poolSize; i++) {
const worker = new Worker(workerUrl);

worker.onmessage = (e: MessageEvent) => {
const { result, error, taskId } = e.data;

// 处理任务
// ...

// 重新加入可用队列
this.availableWorkers.push(worker);
this.processQueue();
};

this.workers.push(worker);
this.availableWorkers.push(worker);
}
}

execute(data: T): Promise<R> {
return new Promise((resolve, reject) => {
this.taskQueue.push({ data, resolve, reject });
this.processQueue();
});
}

private processQueue() {
while (this.taskQueue.length > 0 && this.availableWorkers.length > 0) {
const worker = this.availableWorkers.pop()!;
const task = this.taskQueue.shift()!;

worker.postMessage(task.data);

// 临时存储回调
(worker as any).__task = task;
}
}

terminate() {
this.workers.forEach(w => w.terminate());
this.workers = [];
this.availableWorkers = [];
this.taskQueue = [];
}
}

// 使用
const pool = new WorkerPool<ImageData, ImageData>(
new URL('./image-worker.ts', import.meta.url),
4
);

// 并行处理多个任务
const results = await Promise.all([
pool.execute(imageData1),
pool.execute(imageData2),
pool.execute(imageData3),
]);

Transferable Objects

Transferable Objects 可以零拷贝传输,避免序列化开销。

// 支持的类型
// - ArrayBuffer
// - MessagePort
// - ImageBitmap
// - OffscreenCanvas
// - ReadableStream
// - WritableStream
// - TransformStream

// main.ts
const buffer = new ArrayBuffer(1024 * 1024); // 1MB

// ❌ 普通传输 - 需要复制
worker.postMessage(buffer);

// ✅ 转移传输 - 零拷贝
worker.postMessage(buffer, [buffer]);
// 注意:传输后 buffer 在主线程不可用
console.log(buffer.byteLength); // 0

// 传输多个对象
const buffer1 = new ArrayBuffer(1000);
const buffer2 = new ArrayBuffer(2000);
worker.postMessage({ b1: buffer1, b2: buffer2 }, [buffer1, buffer2]);

图片处理示例

// main.ts
async function processImage(image: HTMLImageElement) {
// 创建 ImageBitmap
const bitmap = await createImageBitmap(image);

// 转移到 Worker
worker.postMessage({ bitmap }, [bitmap]);
}

// worker.ts
self.onmessage = async (e: MessageEvent) => {
const { bitmap } = e.data;

// 创建 OffscreenCanvas
const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
const ctx = canvas.getContext('2d')!;

ctx.drawImage(bitmap, 0, 0);

// 处理图片
const imageData = ctx.getImageData(0, 0, bitmap.width, bitmap.height);
applyFilter(imageData);
ctx.putImageData(imageData, 0, 0);

// 返回处理后的图片
const resultBitmap = await createImageBitmap(canvas);
self.postMessage({ bitmap: resultBitmap }, [resultBitmap]);
};

function applyFilter(imageData: ImageData) {
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
// 灰度处理
const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
data[i] = data[i + 1] = data[i + 2] = avg;
}
}

Shared Worker

多个页面共享同一个 Worker 实例。

// shared-worker.ts
const connections: MessagePort[] = [];

self.onconnect = (e: MessageEvent) => {
const port = e.ports[0];
connections.push(port);

port.onmessage = (e: MessageEvent) => {
const { type, data } = e.data;

if (type === 'broadcast') {
// 广播给所有连接
connections.forEach(conn => {
conn.postMessage({ type: 'message', data });
});
}
};

port.start();
};

// page.ts
const worker = new SharedWorker(new URL('./shared-worker.ts', import.meta.url));
const port = worker.port;

port.onmessage = (e: MessageEvent) => {
console.log('Received:', e.data);
};

port.start();

// 发送消息
port.postMessage({ type: 'broadcast', data: 'Hello from page!' });

Service Worker 缓存

// sw.ts
const CACHE_NAME = 'v1';
const STATIC_ASSETS = [
'/',
'/index.html',
'/styles.css',
'/app.js',
];

// 安装事件:缓存静态资源
self.addEventListener('install', (event: ExtendableEvent) => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => {
return cache.addAll(STATIC_ASSETS);
})
);
});

// 激活事件:清理旧缓存
self.addEventListener('activate', (event: ExtendableEvent) => {
event.waitUntil(
caches.keys().then(keys => {
return Promise.all(
keys
.filter(key => key !== CACHE_NAME)
.map(key => caches.delete(key))
);
})
);
});

// 请求拦截:缓存优先策略
self.addEventListener('fetch', (event: FetchEvent) => {
event.respondWith(
caches.match(event.request).then(cached => {
return cached || fetch(event.request).then(response => {
// 缓存新资源
const responseClone = response.clone();
caches.open(CACHE_NAME).then(cache => {
cache.put(event.request, responseClone);
});
return response;
});
})
);
});

// 注册 Service Worker
// main.ts
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js', { scope: '/' })
.then(registration => {
console.log('SW registered:', registration.scope);
})
.catch(error => {
console.error('SW registration failed:', error);
});
}

缓存策略

策略优点缺点适用场景
Cache First快速响应可能过期静态资源
Network First数据最新依赖网络API 请求
Stale While Revalidate快速+更新略复杂可容忍短暂过期

实际应用场景

1. 大数据处理

// 在 Worker 中处理大量数据
// worker.ts
self.onmessage = (e: MessageEvent) => {
const { data } = e.data;

// 排序、过滤、聚合等操作
const sorted = data.sort((a, b) => a.value - b.value);
const filtered = sorted.filter(item => item.active);
const aggregated = filtered.reduce((acc, item) => {
acc[item.category] = (acc[item.category] || 0) + item.value;
return acc;
}, {});

self.postMessage(aggregated);
};

2. 图像处理

// 在 Worker 中处理图片
self.onmessage = async (e: MessageEvent) => {
const { imageData, filter } = e.data;

switch (filter) {
case 'blur':
applyBlur(imageData);
break;
case 'sharpen':
applySharpen(imageData);
break;
case 'grayscale':
applyGrayscale(imageData);
break;
}

self.postMessage({ imageData }, [imageData.data.buffer]);
};

3. JSON 解析

// 大 JSON 解析
// main.ts
async function parseLargeJSON(jsonString: string) {
const worker = new PromiseWorker<string, object>(
new URL('./json-worker.ts', import.meta.url)
);

return worker.postMessage(jsonString);
}

// json-worker.ts
self.onmessage = (e: MessageEvent) => {
try {
const result = JSON.parse(e.data);
self.postMessage({ result });
} catch (error) {
self.postMessage({ error: (error as Error).message });
}
};

4. WebAssembly

// 在 Worker 中运行 WASM
// worker.ts
let wasmModule: WebAssembly.Instance;

async function initWasm() {
const response = await fetch('/algorithm.wasm');
const bytes = await response.arrayBuffer();
const { instance } = await WebAssembly.instantiate(bytes);
wasmModule = instance;
}

self.onmessage = async (e: MessageEvent) => {
if (!wasmModule) {
await initWasm();
}

const { fn, args } = e.data;
const result = (wasmModule.exports[fn] as Function)(...args);
self.postMessage({ result });
};

5. AI 前端场景

Web Worker 在 AI 前端应用中有多个关键用途,能有效避免 AI 密集计算阻塞 UI。

workers/ai-tasks-worker.ts
// AI 前端中 Worker 的典型应用

// ---- 场景 1:Tokenizer 计算 ----
// 统计输入 token 数量(用于展示剩余 token 额度)
// Tokenizer 初始化和编码是 CPU 密集型操作,必须放 Worker
import { AutoTokenizer } from '@huggingface/transformers';

let tokenizer: Awaited<ReturnType<typeof AutoTokenizer.from_pretrained>> | null = null;

async function initTokenizer() {
// 加载 tokenizer(~5MB),在 Worker 中不阻塞主线程
tokenizer = await AutoTokenizer.from_pretrained('Xenova/gpt-4o');
}

function countTokens(text: string): number {
if (!tokenizer) throw new Error('Tokenizer 未初始化');
const encoded = tokenizer.encode(text);
return encoded.length;
}

// ---- 场景 2:流式 Markdown 解析 ----
// 将 Markdown→HTML 转换放在 Worker 中,避免主线程做重计算
import { marked } from 'marked';

function parseMarkdown(text: string): string {
return marked.parse(text, { async: false }) as string;
}

// ---- 场景 3:端侧 Embedding 推理 ----
// 浏览器端生成文本向量(用于本地知识库搜索)
import { pipeline } from '@huggingface/transformers';

let embedder: Awaited<ReturnType<typeof pipeline>> | null = null;

async function initEmbedder() {
embedder = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2');
}

async function embed(text: string): Promise<Float32Array> {
if (!embedder) throw new Error('Embedding 模型未初始化');
const output = await embedder(text, { pooling: 'mean', normalize: true });
return output.data as Float32Array;
}

// ---- 场景 4:大 JSON 响应解析 ----
// AI 工具调用(Function Calling)返回的结构化数据可能很大
function parseToolResult(jsonString: string): unknown {
return JSON.parse(jsonString);
}

// ---- 统一消息分发 ----
self.onmessage = async (e: MessageEvent) => {
const { type, payload, id } = e.data;

try {
let result: unknown;
switch (type) {
case 'init-tokenizer': await initTokenizer(); result = 'ok'; break;
case 'count-tokens': result = countTokens(payload); break;
case 'parse-markdown': result = parseMarkdown(payload); break;
case 'init-embedder': await initEmbedder(); result = 'ok'; break;
case 'embed': result = await embed(payload); break;
case 'parse-json': result = parseToolResult(payload); break;
}
self.postMessage({ id, result });
} catch (error) {
self.postMessage({ id, error: (error as Error).message });
}
};
AI 场景Worker 中执行的任务为什么需要 Worker
Token 计数Tokenizer 编码长文本编码耗时 10-50ms
流式 MarkdownMarkdown→HTML 转换长文本解析占 CPU
本地推理Embedding/分类/检测模型推理 50-500ms
大 JSON 解析工具调用结果解析>1MB JSON 解析耗时
文件 Hash大文件上传前计算 SHA-25610MB 文件 Hash ~100ms
语音处理音频预处理(重采样、VAD)音频数据量大

更多 AI 端侧推理参考 Web AI 与端侧推理


常见面试问题

Q1: Web Worker 有什么限制?

答案

限制说明
无法访问 DOM不能操作 document、window
无法访问 window 对象使用 self 代替
同源限制Worker 脚本必须同源
无法使用部分 API如 alert、confirm
文件限制不能访问本地文件系统
// Worker 中可用的 API
self.fetch()
self.indexedDB
self.caches
self.WebSocket
self.crypto
self.performance

Q2: 如何实现主线程和 Worker 的通信?

答案

// 1. postMessage + onmessage
// 主线程 → Worker
worker.postMessage(data);
// Worker → 主线程
self.postMessage(result);

// 2. Transferable Objects 高效传输
const buffer = new ArrayBuffer(1000);
worker.postMessage(buffer, [buffer]); // 转移所有权

// 3. SharedArrayBuffer 共享内存
const sab = new SharedArrayBuffer(1024);
const arr = new Int32Array(sab);
worker.postMessage({ buffer: sab });
// 两边都可以读写 arr

Q3: 什么时候应该使用 Web Worker?

答案

场景是否使用 Worker原因
复杂计算避免阻塞 UI
大数据处理不阻塞交互
图片处理计算密集
简单交互通信开销
DOM 操作不支持
小数据量不值得

Q4: Service Worker 和 Web Worker 的区别?

答案

特性Web WorkerService Worker
用途后台计算网络代理/缓存
生命周期随页面独立于页面
API有限Fetch/Cache/Push
作用域页面级域级别
HTTPS不要求必须(localhost 除外)

Q5: 如何优化 Worker 通信性能?

答案

// 1. 使用 Transferable Objects
const buffer = new ArrayBuffer(largeSize);
worker.postMessage(buffer, [buffer]); // 零拷贝

// 2. 批量发送消息
const batch = [];
for (const item of items) {
batch.push(processItem(item));
if (batch.length >= 100) {
worker.postMessage(batch);
batch.length = 0;
}
}

// 3. 使用 SharedArrayBuffer
const sab = new SharedArrayBuffer(1024);
// 主线程和 Worker 共享内存
Atomics.store(new Int32Array(sab), 0, value);

// 4. 避免频繁创建 Worker
// 使用 Worker 池复用

相关链接