学完这篇你能拿到一套可直接抄的 Web Worker 落地方案:把一个几秒钟的同步计算从主线程搬走,页面不卡、带进度条、能取消,并知道哪些坑一定躲开。
第一步:先判断该不该上 Worker
不是所有慢都归 Worker 管。判断标准只有一条:这段代码是否长时间占着主线程不放。
典型该搬走的:大数组遍历/聚合、上万条 JSON 的解析与排序、图片像素级处理(灰度、压缩、马赛克)、加密解密、复杂文本匹配、CSV/Excel 解析。
不该搬的:普通的事件处理、几百毫秒内能跑完的逻辑、依赖 DOM 或 `window` 的操作。
注意:主线程卡顿的表现是「点击没反应、动画掉帧、输入框打字延迟半秒」,而不是「接口慢」。接口慢是网络问题,交给 Worker 没用。
第二步:跑通最小示例
先写 worker 文件,它和主线程不共享作用域,只能靠消息通信。
// worker.js
self.onmessage = (e) => {
const { type, payload } = e.data;
if (type === 'sum') {
let total = 0;
for (let i = 0; i < payload.length; i++) total += payload[i];
self.postMessage({ type: 'done', total });
}
};
主线程:
const worker = new Worker('./worker.js');
worker.onmessage = (e) => console.log(e.data.total);
worker.postMessage({ type: 'sum', payload: new Array(5e7).fill(1.5) });
Worker 里没有 `window`、`document`、`alert`,全局对象是 `self`;`localStorage` 也不可用,缓存请用 `caches` 或 IndexedDB。
注意:直接双击 HTML 用 `file://` 打开,Chrome 会报 `Failed to construct 'Worker'`,这是同源策略限制,不是你代码写错。起个本地服务即可:`python -m http.server 8080`,或 `npx serve`。
第三步:把大数据传得更省
默认 `postMessage` 走的是结构化克隆,大对象会被复制一份,复制本身就要花时间。传 `ArrayBuffer`、`TypedArray`、`ImageBitmap` 时可以「转移所有权」,零拷贝:
const buffer = new ArrayBuffer(1024 * 1024 * 64);
worker.postMessage(buffer, [buffer]); // 第二个参数是 transfer 列表
console.log(buffer.byteLength); // 0,主线程这边已经失去所有权
注意:转移后原变量会变成空壳(`byteLength === 0`),不要再拿它去读数据。传函数、DOM 节点、Symbol 一律会抛 `DataCloneError`。
第四步:加上进度回报和取消
长任务一定要回报进度,否则用户会以为死机了。在循环里按批次发消息:
// worker.js 内
const CHUNK = 1e6;
for (let i = 0; i < payload.length; i += CHUNK) {
if (self.__cancel) { self.postMessage({ type: 'canceled' }); return; }
// ...分段计算
self.postMessage({ type: 'progress', value: (i + CHUNK) / payload.length });
}
主线程发一条 `{ type: 'cancel' }` 把 `self.__cancel` 置为 true 即可优雅退出;实在不听话的第三方逻辑,直接 `worker.terminate()` 强杀,再重新 `new Worker()`。
注意:一个 Worker 是单线程串行的,你连发 10 条消息它会排队处理。如果同时要跑多个任务,建一个 Worker 池(比如 4 个),轮询分发,别每次任务都 `new Worker()`——创建线程本身有开销。
第五步:模块化和构建工具写法
想用 `import` 语法,给 Worker 加类型声明:
const worker = new Worker('./worker.js', { type: 'module' });
Vite 用户更省事:
import MyWorker from './worker?worker';
const worker = new MyWorker();
Webpack 5 里用 `new Worker(new URL('./worker.js', import.meta.url))`。
注意:经典 Worker 用 `importScripts()` 加载外部脚本,`type: 'module'` 的 Worker 里不能用 `importScripts`,两者别混。
第六步:别忘了错误兜底
Worker 里的异常不会冒泡到主线程的控制台,必须监听:
worker.onerror = (err) => {
console.error('worker 出错:', err.message, err.filename, err.lineno);
};
worker.onmessageerror = (e) => console.error('消息反序列化失败', e);
在 Worker 内部也可以 `try/catch` 后主动 `postMessage({ type: 'error', message })`,让 UI 给出可读提示,而不是一直转圈。
小结
- 判断标准:是否长时间独占主线程,是才搬;接口慢别赖 Worker。
- 通信只有 `postMessage` + `onmessage`,Worker 里没有 DOM、没有 `window`。
- 大 `ArrayBuffer`/`TypedArray` 用 transfer 列表零拷贝转移,转移后原对象失效。
- 长任务必须回报进度,保留取消通道,能优雅退出就别硬杀。
- 高频小任务用 Worker 池复用,别反复创建销毁。
- `file://` 跑不起来是正常的,起本地服务;模块化记得加 `{ type: 'module' }`。
- 一定要挂 `onerror`,否则出错你连日志都看不到。