Web Worker 实战:把重活从主线程搬出去
本文目录
浏览器主线程同时承担脚本执行、布局、绘制和输入响应。长时间同步计算会让页面卡住;Worker 可把合适的计算移到后台线程,但它不会自动让算法更快,消息序列化、复制、内存和启动成本也要算进来。
先确认值得迁移
适合:大 JSON/CSV 解析、搜索索引、排序聚合、图片像素处理、压缩、离线文本分析和不依赖 DOM 的推理任务。不适合:极短计算、依赖 window/document 的代码、频繁往返细粒度消息或本质由网络等待的任务。先在低端设备测量主线程长任务和交互阻塞。
Worker 没有 DOM。主线程传递明确的消息协议,包含 taskId、type、schemaVersion、payload;Worker 返回 result 或可分类错误。输入先做大小、类型和复杂度限制,不把任意用户值传入 eval 或动态模块加载。
取消、并发和失败
为请求建立 pending map 与超时;页面卸载时拒绝未完成 Promise 并终止专属 Worker。共享 Worker 要按 taskId 取消并清理监听器。Worker 报错、脚本加载失败和 CSP 阻断都应回到主线程展示;不应静默卡在 loading。
const worker = new Worker(new URL('./parse.worker.js', import.meta.url), { type: 'module' });
let nextId = 0;
const pending = new Map();
function runTask(type, payload, transfer = []) {
const id = ++nextId;
return new Promise((resolve, reject) => {
pending.set(id, { resolve, reject });
worker.postMessage({ id, type, payload }, transfer);
});
}
worker.addEventListener('message', ({ data }) => {
const task = pending.get(data.id);
if (!task) return;
pending.delete(data.id);
data.error ? task.reject(new Error(data.error)) : task.resolve(data.result);
});
生产封装还需要 timeout、取消信号、onerror/messageerror、队列上限与 terminate 清理。任务结果到达时要确认组件仍挂载且账号/路由上下文未变化。
减少数据复制
结构化克隆会复制普通对象;可转移的 ArrayBuffer、MessagePort 等可以转移所有权,降低复制成本,但发送后原引用可能被 detach。只有调用方不再需要原 buffer 时才转移。共享内存需要额外的安全、跨源隔离条件和同步设计,不应为了微优化默认启用。
设置最大输入/输出字节、worker 数与并发任务数。大量小任务可合并,CPU 密集工作不要创建过多 worker;Worker pool 需要公平队列、取消、崩溃重建和组件释放。图片处理要限制像素乘积与解码尺寸,避免小文件名下隐藏超大图片导致内存耗尽。
验证和打包
用 Performance trace 比较主线程长任务、Worker 启动时间、传输耗时和总完成时间;同时看内存与低端手机续航。测试模块 Worker URL、生产构建路径、CSP、离线、后台标签页、快速路由切换和连续报错。Vite/Webpack 的 Worker 打包写法依赖项目配置,优先使用 bundler 官方入口模式并检查产物,不要假设开发服务器成功就代表发布后可加载。
参考:MDN Using Web Workers、Transferable objects、OffscreenCanvas。
Live Notes
留言提交后会先进入审核,审核通过后对其他读者公开。