Web Worker、任务切分与主线程调度

Web Worker、任务切分与主线程调度

这篇笔记还在编辑中,内容可能会继续补充、调整或重写。

为什么主线程会卡顿

JavaScript、样式计算、布局和大部分绘制工作共享浏览器主线程。一段持续执行的同步任务会阻止输入处理和下一帧绘制,使用户感到点击无响应或滚动卡顿。

优化顺序通常是:先减少不必要工作,再改进算法,然后把剩余工作切成可让出主线程的小任务;只有适合并行且传输成本合理的 CPU 密集任务才移入 Worker。

任务切分

大型循环可以按时间预算分批执行,并在批次之间让浏览器处理输入和渲染。选择 setTimeoutrequestAnimationFramerequestIdleCallback 或调度 API 时,要理解它们分别面向延迟执行、下一帧视觉更新、空闲机会和优先级调度。

微任务不会自动“让出一帧”。递归创建 Promise 微任务仍可能长期占用事件循环,因此不能把代码改成 await Promise.resolve() 就认为完成了任务切分。

Web Worker 的边界

Worker 通过消息与页面通信,不能直接访问 DOM。默认结构化克隆会复制数据;大型 ArrayBuffer 可以转移所有权以减少复制成本。频繁发送大量小消息、重复序列化复杂对象,可能抵消并行带来的收益。

适合 Worker 的场景包括:

  • 大型文本解析、压缩或加密
  • 图片、音视频和几何计算
  • 大数据排序、过滤和聚合
  • 不依赖 DOM 的语言服务或编译任务

简单网络请求或少量数组处理通常不值得增加 Worker 生命周期、错误和部署复杂度。

工程化关注点

  • 构建工具如何解析 new Worker(new URL('./worker.ts', import.meta.url))
  • Worker Chunk 的缓存、CSP 和跨源限制
  • 消息协议的类型、版本、取消和错误序列化
  • 页面卸载或任务过期时如何终止计算
  • SSR 环境中如何避免访问浏览器专属 API

实践任务

实现一个会阻塞主线程的大型过滤任务,分别比较同步执行、分批执行和 Worker:

  1. 使用 Performance 面板记录长任务和交互延迟。
  2. 固定数据规模,分别记录总耗时和主线程阻塞时间。
  3. 加入任务取消,确保旧结果不会覆盖新查询。
  4. 比较复制普通数组和转移 ArrayBuffer 的成本。

验收标准

  • 优化前后都有 Performance Trace
  • 能区分总计算时间和主线程可响应时间
  • 新请求可以取消或废弃旧计算结果
  • Worker 错误能被捕获并关联到发布版本
  • 数据传输成本被纳入方案比较

参考资料