Web Worker 是浏览器提供的多线程能力,能把排序、加密、大文件解析等重计算移出主线程,避免页面卡顿、掉帧和点击无响应。本文用可运行的实战代码,讲清 Worker 的创建、通信、数据传输,以及它在 Vue / React 项目里的落地方式,并附上常见坑排查清单。
一、为什么主线程会卡
浏览器里 JavaScript 默认运行在主线程上,渲染、事件处理、脚本执行全都挤在同一条线程里。一旦你写了一次 O(n²) 的排序、一次上万行的 CSV 解析,或者一段密集的数学计算,主线程就会被长时间占用。此时页面会出现”转圈””点不动””动画掉帧”——因为渲染任务排在计算任务后面,只能干等。
在浏览器事件循环一文中我们讲过,宏任务与微任务的排队机制决定了:一个长任务不结束,后续渲染就无法插队。业界把”阻塞主线程超过 50 毫秒”的任务称为长任务(Long Task),它会直接拉低交互响应与核心 Web 指标(如 INP)。Worker 的本质,就是把这类长任务从主线程剥离出去。
二、Web Worker 是什么,又不是什么
Worker 是一个运行在后台的独立线程,拥有自己的全局作用域(self),不能直接操作 DOM,也不能访问 window、document、localStorage。它和主线程通过 postMessage 通信,数据默认走”结构化克隆”拷贝。换句话说,Worker 不是让 DOM 操作变快,而是让”算”不再拖累”画”。
- 能做的:纯计算、数据处理、网络请求、定时器、IndexedDB 读写。
- 不能做的:直接改 DOM、调用
window上的 API、读写主线程变量。
浏览器其实提供了三类 Worker,适用场景不同:
| 类型 | 作用范围 | 典型用途 |
|---|---|---|
| Dedicated Worker | 仅一个页面(创建者) | 页面内的重计算卸载 |
| Shared Worker | 多个同源页面共享 | 跨标签页状态、共享连接 |
| Service Worker | 整个源,代理网络 | 离线缓存、请求拦截(PWA) |
三、最小可运行示例
主线程创建 Worker 并收发消息,逻辑直观:
// main.js
const worker = new Worker('worker.js');
worker.postMessage({ type: 'compute', payload: 40_000_000 });
worker.onmessage = (e) => {
console.log('结果返回:', e.data); // { type:'done', result: ... }
};
worker.onerror = (err) => {
console.error('Worker 出错:', err.message);
};
Worker 脚本本身只监听 message 事件:
// worker.js
self.onmessage = (e) => {
const { type, payload } = e.data;
if (type === 'compute') {
let sum = 0;
for (let i = 0; i < payload; i++) sum += Math.sqrt(i);
self.postMessage({ type: 'done', result: sum });
}
};
四、实战:用 Worker 跑大数组排序
假设要对 500 万个数字排序,放在主线程会让页面冻结近一秒。移到 Worker 后,主线程保持流畅,用户还能正常滚动和点击:
// sort.worker.js
self.onmessage = (e) => {
const arr = e.data; // 大数组
const t0 = performance.now();
arr.sort((a, b) => a - b);
const t1 = performance.now();
self.postMessage({ sorted: arr, costMs: t1 - t0 });
};
// main.js
const big = Array.from({ length: 5_000_000 }, () => Math.random());
const w = new Worker('sort.worker.js');
w.postMessage(big);
w.onmessage = (e) => console.log('排序耗时', e.data.costMs, 'ms');
4.1 数据传输:拷贝还是转移
默认情况下,postMessage 会对数据做结构化克隆——也就是深拷贝一份。当数组很大时,拷贝本身就有成本。对于 ArrayBuffer、TypedArray、ImageBitmap 等二进制对象,可用转移(Transferable)把所有权直接”移交”给 Worker,实现零拷贝:
const buffer = new ArrayBuffer(1024 * 1024 * 64); // 64MB
const view = new Uint8Array(buffer);
// 第二个参数是可转移对象列表
worker.postMessage({ buffer }, [buffer]);
// 注意:转移后主线程的 buffer 已不可用(被清空)
五、拷贝 vs 转移:怎么选
| 方式 | 原理 | 适用场景 | 开销 |
|---|---|---|---|
| 结构化克隆 | 深拷贝一份新数据 | 小对象、需主线程保留副本 | 随数据量线性增长 |
| Transferable | 所有权转移,零拷贝 | 大二进制、一次性传递 | 极低,但源失效 |
六、实战:在 Worker 里发网络请求
Worker 内可以使用 fetch,把”拉取 + 解析”整个过程都搬离主线程,UI 只等最终结果。比如下载一份大日志后只在 Worker 内统计行数:
// fetch.worker.js
self.onmessage = async (e) => {
const { url } = e.data;
const res = await fetch(url);
const text = await res.text();
// 在 Worker 内完成解析,只回传需要的部分
const lines = text.split('\n').length;
self.postMessage({ lines });
};
七、模块化 Worker 与 importScripts
现代浏览器支持用 type: 'module' 创建模块 Worker,可直接 import 第三方库(如把解析、加密逻辑抽成独立模块):
const worker = new Worker('module.worker.js', { type: 'module' });
// module.worker.js
import { heavyCompute } from './lib/compute.js';
self.onmessage = (e) => {
self.postMessage(heavyCompute(e.data));
};
对于不支持模块的旧环境,可用 importScripts('lib.js') 同步加载外部脚本。注意 importScripts 是同步阻塞的,只适合加载轻量工具函数。
八、在 Vue / React 里怎么用
框架本身不直接管理 Worker,但我们都希望”组件卸载时回收线程”。以 Vue 3 为例,在 onMounted 创建、onUnmounted 终止,避免内存泄漏:
// useWorker.js(Vue 组合式)
import { onMounted, onUnmounted, ref } from 'vue';
export function useWorker() {
const result = ref(null);
let worker;
onMounted(() => {
worker = new Worker('/calc.worker.js', { type: 'module' });
worker.onmessage = (e) => (result.value = e.data);
});
onUnmounted(() => worker?.terminate());
return { result, run: (data) => worker.postMessage(data) };
}
React 侧思路一致,配合 React 性能优化 Hooks 把重计算从渲染链路中剥离即可。若计算可中断,还可在 Worker 内用 setTimeout 分片,定期把进度回传主线程更新进度条。
九、常见坑与调试清单
| 现象 | 原因 | 对策 |
|---|---|---|
| Worker 路径 404 | 相对路径解析基址不对 | 用绝对路径或构建工具的分片输出 |
| 数据传过去是空的 | 误用了 Transferable | 检查转移列表是否已清空源对象 |
| 想用 DOM 报错 | Worker 无 window 权限 | 把 DOM 操作留在主线程 |
| 打包后不工作 | Worker 未被正确拆包 | 用 Vite 的 new URL('./x.js', import.meta.url) |
| 页面卸载后仍在跑 | 忘记 terminate | 在生命周期钩子里调用 worker.terminate() |
十、真实业务场景:图片压缩与加解密
Worker 在实际项目里最常见的两类落地是图片处理与加解密。比如用户上传一张几 MB 的照片,主线程直接压缩会让界面卡顿,而用 Worker 调用 OffscreenCanvas 压缩则丝滑无感:
// compress.worker.js
self.onmessage = async (e) => {
const bitmap = await createImageBitmap(e.data.file);
const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
const ctx = canvas.getContext('2d');
ctx.drawImage(bitmap, 0, 0);
// 压缩为 quality=0.7 的 webp
const blob = await canvas.convertToBlob({ type: 'image/webp', quality: 0.7 });
const buf = await blob.arrayBuffer();
self.postMessage({ buffer: buf }, [buf]);
};
另一个高频场景是哈希与加密:把 crypto.subtle 的派生密钥、签名运算放进 Worker,登录注册这类交互就不会因为一次 PBKDF2 计算而卡住按钮。注意 OffscreenCanvas 需要较新的浏览器,低端环境可降级为主线程压缩并给出 loading 提示。
十一、性能对比与小结
我们用 500 万次浮点累加做了一组对照:放在主线程时,页面在约 900ms 内完全无法响应;改用 Worker 后,主线程的点击与滚动始终保持流畅,仅多出一个约 30ms 的消息往返开销。结论很清晰——
| 方案 | 主线程阻塞 | 用户可交互 |
|---|---|---|
| 主线程直接计算 | 约 900ms | 否 |
| Web Worker 卸载 | 约 0ms | 是 |
Web Worker 不是银弹,但它精准解决了”主线程被计算绑架”这一高频卡顿根因。经验法则:单次计算超过 50ms、且与界面更新无关的逻辑,就值得放进 Worker。配合 前端性能优化 的其余手段(懒加载、虚拟列表、防抖节流),能把交互流畅度再抬一截。真正决定用户体验的,往往是这些”看不见的线程”。




