Web Worker 性能优化:通信、并行与内存管理
先搞清楚瓶颈在哪
Worker 性能优化不是玄学,瓶颈就三个地方:创建开销、通信开销、计算开销。先 Profiler 看哪个是瓶颈,再对症下药,别瞎优化。
创建开销:复用比重建快 100 倍
new Worker() 不是免费的。浏览器要分配线程、解析脚本、初始化上下文,一次创建大概 10-50ms。如果你每次任务都新建再 terminate,开销比任务本身还大。
Worker 池
和数据库连接池一个道理——预先创建好,任务来了分配,做完了归还:
javascriptclass WorkerPool { constructor(workerUrl, size = navigator.hardwareConcurrency || 4) { this.workers = []; this.queue = []; for (let i = 0; i < size; i++) { const worker = new Worker(workerUrl); worker.busy = false; worker.onmessage = (e) => { const { resolve } = worker.task; delete worker.task; worker.busy = false; this.processQueue(); resolve(e.data); }; this.workers.push(worker); } } exec(data) { return new Promise((resolve) => { const worker = this.workers.find(w => !w.busy); if (worker) { worker.busy = true; worker.task = { resolve }; worker.postMessage(data); } else { this.queue.push({ data, resolve }); } }); } processQueue() { if (this.queue.length === 0) return; const worker = this.workers.find(w => !w.busy); if (!worker) return; const { data, resolve } = this.queue.shift(); worker.busy = true; worker.task = { resolve }; worker.postMessage(data); } } // 使用 const pool = new WorkerPool('worker.js', 4); const result = await pool.exec({ type: 'sort', data: largeArray });
Worker 池适合任务频繁但单个任务不太大的场景。如果任务很少(比如页面生命周期内就跑一两次),直接 new Worker() 就行,别过度设计。
通信开销:序列化才是大头
Worker 通信的瓶颈不在网络,在序列化。postMessage 默认用结构化克隆,数据量大的时候拷贝耗时惊人。
Transferable:零拷贝传大数据
javascriptconst buffer = new Float64Array(1_000_000); // 慢:结构化克隆,拷贝 8MB 数据 worker.postMessage({ data: buffer }); // 快:转移所有权,零拷贝 worker.postMessage({ data: buffer }, [buffer.buffer]); // 注意:转移后主线程不能再访问 buffer
实测数据:
| 数据大小 | 结构化克隆 | Transferable |
|---|---|---|
| 100KB | ~0.5ms | ~0.05ms |
| 1MB | ~5ms | ~0.1ms |
| 10MB | ~50ms | ~0.2ms |
10MB 以上的数据,不用 Transferable 等于白用 Worker。
SharedArrayBuffer:跳过序列化
Transferable 虽然零拷贝,但只能单向传——发过去主线程就没了。如果你需要双向频繁读写同一块数据,用 SharedArrayBuffer:
javascriptconst shared = new SharedArrayBuffer(1024 * 1024); const view = new Float64Array(shared); // 主线程和 Worker 共享同一块内存 worker.postMessage({ shared }); // Worker 里直接读写 self.onmessage = (e) => { const view = new Float64Array(e.data.shared); Atomics.store(view, 0, 42); // 原子写入 };
需要配合 Atomics 做原子操作,服务端还要配 COOP/COEP 头,门槛比 Transferable 高。但高频通信场景下收益巨大——完全没有序列化开销。
批量发送:减少通信次数
每秒 postMessage 100 次和 1 次发 100 条数据,后者快得多。序列化有固定开销(即使数据很小也要走一遍结构化克隆流程),减少次数比减少数据量更有效:
javascript// 慢:逐条发送 data.forEach(item => worker.postMessage(item)); // 快:攒批发送 worker.postMessage({ batch: data });
计算开销:用多 Worker 并行
单 Worker 的计算速度和主线程 JS 一样,只是不卡 UI。要真正加速,得把任务拆给多个 Worker 并行跑:
javascriptfunction parallelSort(data, workerCount = 4) { const chunkSize = Math.ceil(data.length / workerCount); const chunks = []; for (let i = 0; i < workerCount; i++) { chunks.push(data.slice(i * chunkSize, (i + 1) * chunkSize)); } return Promise.all(chunks.map((chunk, i) => { return new Promise((resolve) => { const worker = new Worker('sort-worker.js'); worker.onmessage = (e) => resolve(e.data); worker.postMessage(chunk); }); })).then(sortedChunks => { // 合并已排序的分片 return mergeSortedArrays(sortedChunks); }); }
实测 100 万元素数组排序:
| 方案 | 耗时 |
|---|---|
| 主线程单线程 | ~800ms(UI 卡死) |
| 单 Worker | ~800ms(UI 正常) |
| 4 Worker 并行 | ~250ms(UI 正常) |
Worker 数量不要超过 CPU 核心数,navigator.hardwareConcurrency 可以拿到。多了反而会因为线程调度开销变慢。
内存管理
Worker 占的内存不会自动释放,必须显式 terminate()。如果页面生命周期内不再需要某个 Worker,立刻关掉:
javascript// 任务完成后关闭 worker.onmessage = (e) => { handleResult(e.data); worker.terminate(); // 释放线程和内存 }; // 或者超时强制关闭 const timeout = setTimeout(() => worker.terminate(), 30000); worker.onmessage = (e) => { clearTimeout(timeout); handleResult(e.data); };
长时间运行的 Worker 要注意内存泄漏——Worker 里的闭包、事件监听器、定时器如果不用了不清理,内存会持续上涨。在 Worker 里加个定期自检:
javascriptsetInterval(() => { const used = performance.memory?.usedJSHeapSize; if (used && used > 50 * 1024 * 1024) { // 超过 50MB self.postMessage({ type: 'memory-warning', used }); } }, 10000);
懒加载:按需创建 Worker
不是所有 Worker 都要在页面加载时就创建。用 new URL() + 动态 import 实现按需加载,首屏不需要的 Worker 等用到时再创建:
javascriptasync function getWorker() { if (!workerInstance) { workerInstance = new Worker( new URL('./heavy-worker.js', import.meta.url), { type: 'module' } ); } return workerInstance; } // 用户点击"导出"按钮时才创建 button.onclick = async () => { const worker = await getWorker(); worker.postMessage(exportData); };
优化优先级
按收益从大到小排:
- Transferable 替代结构化克隆(大数据场景立竿见影)
- Worker 池复用(频繁创建销毁场景收益大)
- 批量发送减少通信次数(高频小消息场景)
- 多 Worker 并行(计算密集型场景)
- SharedArrayBuffer(超高频双向通信场景,门槛高但收益最大)
- 懒加载(首屏性能敏感场景)