5月27日 13:59

什么是 Web Worker?它如何解决页面卡顿问题?

JavaScript 的单线程困局

浏览器里,JS 和 UI 渲染共享同一个线程。这意味着一件事:JS 代码跑多久,页面就卡多久。当你排序 10 万条数据、解析 20MB 的 JSON、或者做复杂的图像运算时,用户看到的不是加载动画,而是冻住的页面——滚动没用,点击没用,连浏览器标签页都显示"无响应"。

Web Worker 就是冲着这个问题来的:给 JS 开一条独立线程,把耗时任务丢过去跑,主线程继续处理 UI。

Worker 到底是什么

Worker 是浏览器提供的一个独立执行环境,和主线程平级运行。几个关键事实:

  • 独立线程:Worker 有自己的调用栈和事件循环,不会阻塞主线程
  • 独立全局对象:Worker 里没有 window,取而代之的是 selfDedicatedWorkerGlobalScope
  • 不能碰 DOMdocumentelementlocalStorage 一概不可用
  • 只能用消息通信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:

javascript
const 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 核心数来定:

javascript
const 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 实例。适合多标签页同步状态的场景,比如购物车数量、未读消息数。创建方式不同:

javascript
const 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 万元素数组做排序:

方案耗时主线程影响
主线程直接排序~800msUI 完全卡死
Worker 排序~800msUI 正常响应
4 个 Worker 分片排序~250msUI 正常响应

Worker 不加速计算,但释放主线程。多 Worker 并行才是真正的加速——代价是代码复杂度上去了,需要分片和合并结果。

标签:Web Worker