什么是 Web Worker?它如何解决页面卡顿问题?
JavaScript 的单线程困局
浏览器里,JS 和 UI 渲染共享同一个线程。这意味着一件事:JS 代码跑多久,页面就卡多久。当你排序 10 万条数据、解析 20MB 的 JSON、或者做复杂的图像运算时,用户看到的不是加载动画,而是冻住的页面——滚动没用,点击没用,连浏览器标签页都显示"无响应"。
Web Worker 就是冲着这个问题来的:给 JS 开一条独立线程,把耗时任务丢过去跑,主线程继续处理 UI。
Worker 到底是什么
Worker 是浏览器提供的一个独立执行环境,和主线程平级运行。几个关键事实:
- 独立线程:Worker 有自己的调用栈和事件循环,不会阻塞主线程
- 独立全局对象:Worker 里没有
window,取而代之的是self(DedicatedWorkerGlobalScope) - 不能碰 DOM:
document、element、localStorage一概不可用 - 只能用消息通信:
postMessage发,onmessage收,数据走结构化克隆 - 同源限制:Worker 脚本必须和页面同源
怎么用
创建和通信
javascript// 主线程 const worker = new Worker('worker.js'); // 发数据给 Worker worker.postMessage({ type: 'sort', data: largeArray }); // 接收 Worker 返回的结果 worker.onmessage = (e) => { console.log('结果:', e.data.result); }; // 出错处理 worker.onerror = (e) => { console.error(`Worker 错误: ${e.message} (${e.filename}:${e.lineno})`); }; // 不用了就关掉 worker.terminate();
javascript// worker.js self.onmessage = (e) => { const { type, data } = e.data; if (type === 'sort') { const result = data.sort((a, b) => a - b); self.postMessage({ result }); } };
内联 Worker:不想多一个文件
有时候 Worker 代码很短,单独建文件嫌麻烦。可以用 Blob URL 创建内联 Worker:
javascriptconst code = ` self.onmessage = (e) => { const result = heavyCalc(e.data); self.postMessage(result); }; `; const blob = new Blob([code], { type: 'application/javascript' }); const worker = new Worker(URL.createObjectURL(blob));
这在单文件组件或沙箱环境里特别好用。
多个 Worker 并行
一个 Worker 不够就开多个。浏览器对 Worker 数量没有硬限制,但每个 Worker 都占一个线程,开太多反而有调度开销。通常根据 CPU 核心数来定:
javascriptconst cores = navigator.hardwareConcurrency || 4; const workers = Array.from({ length: cores }, () => new Worker('worker.js')); // 把任务分片给多个 Worker const chunkSize = Math.ceil(data.length / cores); const results = await Promise.all( workers.map((worker, i) => { const chunk = data.slice(i * chunkSize, (i + 1) * chunkSize); return new Promise((resolve) => { worker.onmessage = (e) => resolve(e.data.result); worker.postMessage({ data: chunk }); }); }) );
什么时候该用 Worker
不是所有耗时操作都需要 Worker。判断标准很简单:会不会阻塞主线程超过 50ms? 会就上 Worker,不会就不必。
值得用 Worker 的场景:
- 大数据排序、过滤、聚合(超过 1 万条记录的客户端处理)
- 文件解析(CSV、JSON、Excel)
- 图像处理(Canvas 像素操作、滤镜)
- 加密运算(RSA、AES 大数据量加密)
- 实时数据流处理(WebSocket 推送数据的聚合计算)
不需要 Worker 的场景:
- fetch 请求——本来就异步,不阻塞主线程
- 简单的 DOM 操作——Worker 做不了
- 定时器——setTimeout/setInterval 本身不阻塞
- 少量数据运算(几百条数据的遍历)
Worker 的限制和绕过方式
| 限制 | 绕过方式 |
|---|---|
| 不能访问 DOM | 把计算结果 postMessage 回主线程,主线程操作 DOM |
| 不能用 localStorage | 用 IndexedDB 替代,Worker 可以访问 |
| 不能用 XMLHttpRequest | 用 fetch 替代,Worker 支持 |
| 不能用 window 对象 | 用 self 替代全局对象 |
| 同源限制 | 用 Blob URL 创建内联 Worker |
| 通信有序列化开销 | 大数据用 Transferable 零拷贝,高频通信用 SharedArrayBuffer |
Worker 的三种类型
Dedicated Worker:最常见的,和一个页面绑定,页面关了 Worker 也销毁。
Shared Worker:多个页面共享同一个 Worker 实例。适合多标签页同步状态的场景,比如购物车数量、未读消息数。创建方式不同:
javascriptconst worker = new SharedWorker('shared-worker.js'); worker.port.onmessage = (e) => { /* 收消息 */ }; worker.port.postMessage({ type: 'sync' });
Service Worker:本质是网络代理,拦截请求、管理缓存。PWA 的核心,和普通 Worker 用途完全不同,别混为一谈。
常见踩坑
坑 1:频繁通信拖垮性能。每秒 postMessage 几百次,序列化开销比计算本身还大。解决方案:批量发送,攒够一批再传;或者改用 SharedArrayBuffer 共享内存。
坑 2:Worker 里抛的异常主线程收不到。必须在主线程监听 worker.onerror,否则 Worker 静默挂掉你都不知道。
坑 3:Transferable 传完后原数据变空。postMessage({ buffer }, [buffer]) 之后,主线程的 buffer.byteLength 变成 0。如果主线程还需要这个数据,先拷贝一份再传。
坑 4:Worker 脚本路径是相对 HTML 的,不是相对 JS 文件的。在打包工具(Webpack/Vite)里容易路径搞错,建议用 new URL('./worker.js', import.meta.url) 让打包工具正确处理。
javascript// Vite/Webpack 5 的正确写法 const worker = new Worker( new URL('./worker.js', import.meta.url), { type: 'module' } );
性能实测
在 Chrome 120 / M1 MacBook Pro 上,对 100 万元素数组做排序:
| 方案 | 耗时 | 主线程影响 |
|---|---|---|
| 主线程直接排序 | ~800ms | UI 完全卡死 |
| Worker 排序 | ~800ms | UI 正常响应 |
| 4 个 Worker 分片排序 | ~250ms | UI 正常响应 |
Worker 不加速计算,但释放主线程。多 Worker 并行才是真正的加速——代价是代码复杂度上去了,需要分片和合并结果。