Web Worker 这么好,你怎么还不用?

2,184 阅读5分钟

Web Worker 有多简单?一张图就能讲明白:

Web Worker 图示

但是却很少有人用,为什么?估计大家心中还有各种疑问,不妨我们来一一拆解:

兼容性如何?

在 caniuse.com 上可以看到,IE 10 以上都兼容了。如果你不需要兼容老旧 IE,直接上 Web Worker 都是没问题的。

Web Worker 浏览器兼容性

只是个别属性,需要更高版本的 chromium。如,监听 message error 需要 60+,支持 es module 需要 80+, 详见: Web Worker 浏览器兼容性

如何创建?是否需要 worker loader?

如果是普通的项目,我们可直接创建 Worker:

const worker = new Worker('work.js');

在 Webpack 项目中,我们都需要添加各种 loader 支持新技术,创建 Worker 时要使用 worker-loader:

// webpack 4.0
import Worker from 'worker-loader!./worker' // ./worker 这是worker模块的路径
const worker = new Worker()

但,Webpack 5.0 之后我们不再需要 worker-loader 了。另外 Vite 也是支持直接引入 Worker 的,于是我们可以这样创建:

// Webpack 5.0+、Vite
// 方法一
import Worker from './woker?worker' // ./worker 这是worker模块的路径
const worker = new Worker();

// 方法二
const worker = new Worker(new URL('./worker.js', import.meta.url));

此处 new URL,可以约等于 nodejs 中的 path.resolve(baseurl + './worker.js')

是否支持模块化?

如前文所述,Chrome 80+ 已经支持 ES Module 模块化了。当 Worker 的内容过大,可以按模块进行拆分。

如果用 import 引入,报 Cannot use import statement outside a module 不允许在 module 外使用 import

这个时候你需要在创建 Worker 时,指定 type:

const worker = new Worker(new URL('./worker.js', import.meta.url), {
    type: module
});

// worker.js
import { forEach } from 'lodash'
...

这时就能正常使用import、export了。

如果你要支持 80 之前版本的 Chrome,可以使用 importScripts 来加载其他脚本。不过这个引入并不如 ES Module 那样清晰,引入的代码会全部执行,无法按需引入。

// 兼容 80- 版本的 Chrome
// worker.js
importScripts('script1.js');

什么时候用?

最理想的情况是,类似移动端的线程处理,主线程只渲染 UI,其他的内容全部丢给 Worker。如果日常开发已经做到分离渲染层和逻辑层,代码结构清晰,自然就很清楚可以把哪些代码放入 Worker。

但这是理想情况,要达到这个目标,不可能一蹴而就,目前更推荐的是渐进式增强的做法,先把导致页面卡顿的任务交给 Worker 处理

那什么是卡顿呢?

以帧率 60 FPS 计算的话,浏览器需要每 16.67 ms 就更新一次,你会感觉很顺畅。反之,如果超过这个值,就会感觉到页面卡顿(用过高帧率屏幕的人会感觉更明显)。如果超过 50 ms 就会有明显的卡顿,Chrome 也会将这任务标记为 Long task 长任务。这个任务就可以考虑交给 Worker 处理了。

Chrome 标记的长任务

更细致的做法,把通信时间考虑在内,把计算时间 - 通信时间 > 50ms的任务交给 Worker,详见: 一文彻底了解Web Worker

如何操作 DOM?

如上所述,Worker 的愿景是分离 UI 层和逻辑层

因此,Worker 不能直接操作 DOM,只能通过消息发送机制,通知主线程数据变化,主线程再做 UI 层的处理。

可以创建多少个 Worker?

刚开始时我们只会开一个 Worker 来处理任务。但是实际上是允许开多个 Worker 的,能开多少 Worker 取决于我们电脑的CPU、内存。注意的是:过多的线程并不能线性地提升性能。

有些批量的任务,我们可以通过遍历数组,批量创建 worker 来实现。而更复杂的玩法有:

  • Worker 内嵌 Worker;
  • 通过 Shared Worker,实现浏览器内多个 tab 共享数据等。

独立 worker 与 shared worker

这些可以自行查找资料,进行尝试。

是否需要关闭 Worker?

关闭 worker的方法有两种:

  • 在主线程通过 worker.terminal() 关闭;
  • 在 worker 内可以通过self.close() 关闭;

对强迫症而言,会有每使用完成一个 Worker 即关闭的好习惯。但是,如果我们频繁的开闭 Worker 也会消耗资源。

如果需要大批量使用 Worker,更推荐使用常驻线程,使用统一的入口,按照数据类型进行不同的处理。

可以做那些事情?有哪些成熟的例子吗?

1. 知乎加载 Blob 脚本

在知乎的页面中,可以看到,页面已经静态化。

知乎页面静态化

但是,主线程加载完成之后,还用了 worker 加载了脚本。

worker 加载脚本

blob 脚本

在这里我们无法推测这段代码到底做了什么,但是可以清楚这个代码和主要功能没有关系,这么做的话,如果这段代码出问题了,也不会导致页面无法渲染的问题。

2. 百度地图

百度地图中,调用 Web Worker 去调用 Web Assembly,提高地图渲染性能。

百度地图 Web Worker

总结

Web Worker 在兼容性方面无大问题,创建简单,支持 ES Module。

虽然我们暂时无法通过 Web Worker 实现 UI 层和逻辑层分而治之。但是在计算时长过长的时候,可以考虑使用 Web Worker 来减轻主线程压力。

Web Worker 玩法多样,大家多多动手,一起实践!

参考