🔍“拆不拆?”“放不放 Worker?”——带你终结面试灵魂拷问

39 阅读3分钟

一、先抛结论,再讲原理

CPU 密集 + 可拆分 = 多 Worker 并行;IO 密集就别折腾线程,异步事件循环足够。
记住这句话,下面所有内容都是它的推导过程 + 实战 demo。

二、30 s 看懂 CPU 密集 vs. IO 密集

| 类型 | 瓶颈 | 典型场景 | 主线程阻塞 | 上 Worker 收益 | |||||| | CPU 密集 | 运算单元打满 | 加解密、图像滤镜、哈希、物理模拟 | ✅ 会卡 | 极大(卡→不卡) | | IO 密集 | 等网络/磁盘 | fetch、IndexedDB、文件读写 | ❌ 不卡 | ≈ 0(等待时间不变) |

一句话:
“计算型才值得开线程,等待型靠异步回调就够。”

三、二维决策表:把“拆不拆”画成坐标系

横轴:CPU or IO
纵轴:可拆分 or 不可拆分

| | CPU | IO | |||| | 可拆分 | ⭐⭐⭐ 多 Worker 并行 | 异步事件循环即可 | | 不可拆分 | ⭐ 单 Worker 也行 | 异步事件循环即可 |

左上角(CPU+可拆分) 才是 Worker 的“甜蜜区”。
其余三个象限,要么没必要,要么收益有限。

四、实战套路:4 步公式

遇到任何重活,按顺序打钩:

  1. 是不是 CPU 密集?
    ├─ 否 → 直接异步 IO,pass
    └─ 是 → 下一步

  2. 能不能拆?(数据/算法无关性)
    ├─ 否 → 单 Worker 丢进去,至少不卡 UI
    └─ 是 → 下一步

  3. 拆多少份?
    const POOL = navigator.hardwareConcurrency - 1
    留 1 核给主线程谈恋爱(渲染),其余全上 Worker 池。

  4. 通信大不大?
    → 每块 <1 MB 直接 postMessage
    → 大块用 TransferableSharedArrayBuffer,避免克隆开销。

五、完整案例:2 GB 大文件多线程哈希

需求:前端算 2 GB 视频的 SHA-256,不能卡。

| 指标 | 单线程 | 7 Worker | |||| | 总耗时 | 25 s | 4 s | | 主线程占用 | 100 % 卡死 | <5 ms 丝滑 | | 进度条 | 无 | 实时 0→100 % |

Step-by-step:

  1. 拆块
    块大小 2 MB → 1024 块
    Worker 池 = 7(8 核 CPU -1)

  2. 并行
    主线程轮询空闲 Worker,投递“块号 + 块数据”
    Worker 内 crypto.subtle.digest 计算 SHA-256

  3. 合并
    收到 1024 个块哈希后,主线程一次性 Merkle Tree 归并,吐出最终哈希

  4. 通信优化
    块数据使用 ArrayBuffer + transfer,零拷贝;进度使用 postMessage 传数字,<1 ms

六、速查表:日常任务对号入座

| 任务 | 类型 | 可拆分 | 决策 | ||||| | 图像高斯模糊 | CPU | ✅ | 多 Worker 按行分片 | | 视频解码 | CPU+IO | ✅ | Worker 解码,主线程只渲染 | | 排序 100 万条记录 | CPU | ✅ | 归并排序拆→多 Worker→归并 | | IndexedDB 批量写 | IO | ✅ | 不需要 Worker,事务异步即可 | | AES 加密大文件 | CPU | ✅ | 分块并行加密 | | 调用后端 API | IO | ❌ | fetch 直接异步,别放 Worker |

七、代码模板:拿来即用

主线程 main.js

import { spawn, Pool, Worker } from 'threads';
const pool = Pool(() => spawn(new Worker('./hash-worker.js')), {
  size: navigator.hardwareConcurrency - 1
});

async function calcFileHash(file) {
  const chunkSize = 2 * 1024 * 1024;
  const chunks = Math.ceil(file.size / chunkSize);
  const promises = [];

  for (let i = 0; i < chunks; i++) {
    const slice = file.slice(i * chunkSize, (i + 1) * chunkSize);
    promises.push(pool.queue(worker => worker(slice)));
  }

  const hashes = await Promise.all(promises);
  return mergeMerkle(hashes); // 主线程轻量合并
}

Worker hash-worker.js

import { expose } from 'threads/worker';
import crypto from 'crypto'; // 浏览器即 crypto.subtle

expose(async function (chunk) {
  const buf = await chunk.arrayBuffer();
  const hash = await crypto.subtle.digest('SHA-256', buf);
  return new Uint8Array(hash);
});

八、避坑 3 连

  1. 线程别泛滥
    创建销毁一次 2~5 ms,手机 8 核顶格,超了反而抢占主线程。

  2. 通信别裸克隆
    1 MB 数据 postMessage 耗时 1~10 ms,大图直接 transfer

  3. 调试别懵逼
    DevTools → Sources → Workers 面板,断点、console 一应俱全。

九、一句话总结

“CPU 密集 + 可拆分” 才配让主线程“下班”,其余情况异步事件循环就能搞定。
下次任何重活,先规划,再写代码——
让你的页面从此“假死”归零,丝滑到用户怀疑人生。