Web Workers 与并行计算
浏览器里的“卡”,很多时候不是慢,而是堵
前端性能问题常常被笼统地说成“页面慢”。
但从用户感受上看,很多更痛的问题其实是:
- 输入不跟手
- 滚动发黏
- 点击没反应
- 动画掉帧
- 页面像被卡住了一样
这类问题经常和主线程被占住有关。
因为浏览器里的 UI 渲染、事件处理、很多脚本执行,都要在主线程上争时间。
一旦某段同步任务太重,用户就会直接感知到阻塞。
这也是 Web Workers 最核心的价值:
不是让 JavaScript 变神奇。
而是把某些工作从主线程移开,让界面保持可交互。
先把 Worker 讲简单
Worker 可以理解成:
浏览器里一个独立于页面主线程运行的 JavaScript 执行环境。
它有自己的全局作用域、自己的事件循环,能和主线程通过消息通信。
这句话里最重要的是“独立”。
也正因为独立,Worker 不是免费共享一切。
它和主线程之间有清晰边界。
Worker 不是什么
这点也要先说清楚。
Worker 不是:
- 任意共享 DOM 的副线程
- 想调用什么页面对象就能调用的环境
- 自动把代码并行化的魔法
大多数 Worker 里不能直接操作 DOM。
这不是缺点。
这是它的边界。
也正因为它不直接碰 DOM,才更适合承载计算、解析、转换这类工作。
常见 Worker 类型
Dedicated Worker
最常见。
通常被单个页面或单个脚本拥有。
适合:
- 图像处理
- 大数据计算
- 文本解析
- 编辑器里的后台任务
SharedWorker
可以被多个页面或 tab 共享。
它的价值在于共享连接和状态。
但工程复杂度也更高,所以日常项目里不如 Dedicated Worker 常见。
Service Worker
Service Worker 也叫 worker,但它的职责和前两者不同。
它更偏网络代理、缓存、离线、推送。
不要把它和“拿去做重计算的 Worker”混在一起。
Web Workers 最适合处理什么
比较典型的场景有:
- 大数组排序、过滤、聚合
- Markdown / 富文本解析
- 代码高亮、lint、格式化
- 图像编解码和处理
- 音视频波形或特征计算
- 密码学、压缩、哈希
- Canvas 离屏绘制
一个很实用的判断标准是:
如果某段工作明显会抢主线程时间,而且又不需要直接操作 DOM,就值得考虑 Worker。
什么时候不值得上 Worker
也不是所有计算都值得搬。
因为 Worker 也有成本:
- 初始化成本
- 通信成本
- 构建和调试成本
- 状态同步复杂度
如果任务很轻、很短、很少执行,直接放主线程可能更简单。
成熟判断不是“凡是性能问题都上 Worker”。
而是评估:
- 重不重
- 频不频
- 搬过去之后值不值
消息通信为什么是 Worker 的第一道门槛
主线程和 Worker 默认通过 postMessage 通信。
这意味着你需要把任务写成一种消息协议:
- 发什么数据过去
- 期望收到什么结果
- 失败时怎么回传
- 如何取消或忽略过期任务
这也是为什么 Worker 设计往往不仅是“把函数搬过去”。
而是把一段逻辑重新组织成异步消息系统。
Structured clone 到底在做什么
很多数据在主线程和 Worker 之间传递时,会走 structured clone。
简单说,就是浏览器用一套规定好的算法,把可复制的数据结构拷过去。
MDN 对这一点讲得很清楚:
不是所有对象都能这样传。
像函数、DOM 节点这类就不行。
这会直接影响 Worker API 设计。
如果你的主线程逻辑严重依赖闭包、函数回调和 DOM 对象,搬进 Worker 往往就没那么顺。
Transferable 为什么重要
如果所有数据都一律复制,通信成本会很高。
这时 transferable object 就很关键了。
像 ArrayBuffer 这类对象,可以把底层资源直接“转移”过去,而不是复制一份。
转移后,原线程上的那个对象会被 detach。
这非常适合:
- 大块二进制数据
- 图像、音频 buffer
- 计算密集场景里的零拷贝传递
所以 Worker 不是只会“消息很慢”。
关键是你有没有用对传输方式。
SharedArrayBuffer 为什么更进一步
ArrayBuffer 是转移。
SharedArrayBuffer 则是共享。
它允许不同执行上下文看到同一块共享内存。
这会打开更底层的并行编程能力。
但也会引入同步和安全问题。
所以它不是日常 Worker 编程的默认选项。
你通常只会在这些场景认真考虑它:
- 高性能计算
- Wasm 线程
- 需要显式共享内存的复杂场景
另外,浏览器对 SharedArrayBuffer 有明确安全要求。
MDN 也强调了 cross-origin isolation 条件。
这点不能忽略。
OffscreenCanvas 为什么很实用
很多图形问题的痛点,不只是计算。
还包括绘制本身抢了主线程时间。
OffscreenCanvas 的价值就在于:
把 canvas 绘制能力带到 Worker 里。
这对下面这些场景非常有帮助:
- 图表渲染
- 粒子系统
- 图像编辑器
- 视频帧处理
它让“计算在 Worker,绘制还在主线程”的割裂感减少很多。
requestAnimationFrame 和 Worker 是什么关系
很多人会把这两个概念混在一起。
其实它们关注点不同。
requestAnimationFrame更偏主线程上的逐帧调度- Worker 更偏把某些任务并行或异步地移出去
两者可以配合,但不是替代关系。
例如:
- 主线程保留输入和可见层控制
- Worker 负责计算下一帧需要的数据
这类分工在复杂图形里很常见。
Comlink 为什么值得知道
原生 postMessage 很基础,也很真实。
但写多了会觉得样板多、协议代码多。
Comlink 这类库的价值就在于:
把 Worker 调用包装得更像调用一个远端对象。
它不会消除边界。
但能降低很多消息封装负担。
在团队里,这类抽象常常会明显改善可维护性。
Worker 真正难的地方,不是创建,而是任务模型
写一个 Worker demo 很简单。
真正进入生产后,难点通常是:
- 任务怎么分块
- 结果如何回传
- 如何取消过期任务
- 如何限制并发
- 一个 Worker 够不够,还是需要 worker pool
例如搜索输入时,如果用户连续输入 10 次,你不希望前 9 次的旧结果晚到后覆盖新结果。
所以 Worker 设计最终会回到系统问题:
任务编排和状态一致性。
前端里典型的 Worker 模式
单任务型
一个任务发进去,一个结果回来。
适合简单计算。
长驻服务型
Worker 长期存在,持续接任务。
适合编辑器、解析器、复杂图形。
池化型
多个 Worker 分担任务。
适合更强的并行场景,但复杂度也更高。
调试和构建为什么会增加心智成本
Worker 一旦进入真实项目,就要考虑:
- 打包方式
- 路径和模块加载
- sourcemap
- 错误上报
- 本地与生产环境一致性
这也是为什么 Worker 虽然很强,但并不适合作为“默认架构”。
只有当主线程压力真的成问题,它的收益才会显得足够大。
Worker 和 Wasm 为什么经常一起出现
因为两者都在处理“把重活从主线程挪开”的问题。
Wasm 带来的是更接近底层的执行能力。
Worker 带来的是更合理的线程边界。
二者结合后,适合:
- 编码器
- 图像处理
- 大规模文本处理
- 音视频算法
但不要把“上 Wasm + Worker”当成银弹。
工程成本和调试成本都不低。
中国互联网语境里的常见场景
在国内业务里,Worker 很多时候会出现在这些地方:
- 富文本编辑器
- 图片压缩与上传前处理
- 大表格导出
- 活动页复杂动画与粒子
- 音视频或实时互动页面
这些场景有一个共同点:
用户感知非常直接,主线程一堵就很明显。
海外产品语境里,为什么 Worker 话题常和平台能力一起讨论
很多海外前端语境里,会更明确地把 Worker 放进:
- off-main-thread
- Baseline
- performance budget
- collaborative apps
也就是把它视为平台能力的一部分,而不是冷门技巧。
这很值得借鉴。
因为 Worker 的价值,本来就不是“会不会这个 API”。
而是“你能不能合理利用浏览器的并行边界”。
这类主题为什么特别适合做表达训练
因为它容易被说成:
- JS 是单线程,但 Worker 可以多线程
这句不算错。
但它太粗。
更成熟的表达应该继续说明:
- Worker 为什么能改善交互卡顿
- 它为什么不能直接操作 DOM
- structured clone、transferable、SharedArrayBuffer 分别意味着什么
- OffscreenCanvas 在什么情况下改变了方案选择
建议实践
实践 1:把一个重计算任务搬进 Dedicated Worker
练什么:
感受主线程阻塞和 Worker 分流的差异。
最小交付物:
一个大数组排序或 Markdown 解析 demo。
验收标准:
- 主线程交互明显更顺
- 能解释通信与初始化成本
常见误区:
- 只看结果时间,不看主线程是否真的解堵
实践 2:做一次 transferable 对照实验
练什么:
理解复制与转移的差别。
最小交付物:
一个 ArrayBuffer 在 clone 与 transfer 两种方式下的对照 demo。
验收标准:
- 能观察到原对象被 detach
- 能解释为什么大数据场景适合 transfer
常见误区:
- 把 transfer 误解成“复制得更快”
实践 3:用 OffscreenCanvas 做一个离屏绘制 demo
练什么:
理解计算和绘制一起移出主线程后的价值。
最小交付物:
一个简单图表或粒子动画 demo。
验收标准:
- 绘制逻辑在 Worker 内运行
- 主线程仍保持良好交互
常见误区:
- 只把计算放进 Worker,绘制仍在主线程抢资源