Web Worker 有多简单?一张图就能讲明白:
但是却很少有人用,为什么?估计大家心中还有各种疑问,不妨我们来一一拆解:
兼容性如何?
在 caniuse.com 上可以看到,IE 10 以上都兼容了。如果你不需要兼容老旧 IE,直接上 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 处理了。
更细致的做法,把通信时间考虑在内,把计算时间 - 通信时间 > 50ms的任务交给 Worker,详见: 一文彻底了解Web Worker
如何操作 DOM?
如上所述,Worker 的愿景是分离 UI 层和逻辑层。
因此,Worker 不能直接操作 DOM,只能通过消息发送机制,通知主线程数据变化,主线程再做 UI 层的处理。
可以创建多少个 Worker?
刚开始时我们只会开一个 Worker 来处理任务。但是实际上是允许开多个 Worker 的,能开多少 Worker 取决于我们电脑的CPU、内存。注意的是:过多的线程并不能线性地提升性能。
有些批量的任务,我们可以通过遍历数组,批量创建 worker 来实现。而更复杂的玩法有:
- Worker 内嵌 Worker;
- 通过 Shared Worker,实现浏览器内多个 tab 共享数据等。
这些可以自行查找资料,进行尝试。
是否需要关闭 Worker?
关闭 worker的方法有两种:
- 在主线程通过
worker.terminal()关闭; - 在 worker 内可以通过
self.close()关闭;
对强迫症而言,会有每使用完成一个 Worker 即关闭的好习惯。但是,如果我们频繁的开闭 Worker 也会消耗资源。
如果需要大批量使用 Worker,更推荐使用常驻线程,使用统一的入口,按照数据类型进行不同的处理。
可以做那些事情?有哪些成熟的例子吗?
1. 知乎加载 Blob 脚本
在知乎的页面中,可以看到,页面已经静态化。
但是,主线程加载完成之后,还用了 worker 加载了脚本。
在这里我们无法推测这段代码到底做了什么,但是可以清楚这个代码和主要功能没有关系,这么做的话,如果这段代码出问题了,也不会导致页面无法渲染的问题。
2. 百度地图
百度地图中,调用 Web Worker 去调用 Web Assembly,提高地图渲染性能。
总结
Web Worker 在兼容性方面无大问题,创建简单,支持 ES Module。
虽然我们暂时无法通过 Web Worker 实现 UI 层和逻辑层分而治之。但是在计算时长过长的时候,可以考虑使用 Web Worker 来减轻主线程压力。
Web Worker 玩法多样,大家多多动手,一起实践!