Web Workers 初体验

这个视频有 15+MB 的 BAS 弹幕脚本需要解析,这会导致页面卡死 7 秒左右的时间,期间 UI 被冻结,体验很糟糕,如果使用 Web Workers 进行优化,把解析放入 Web Workers 执行,就可以避免 UI 线程阻塞造成的页面冻结。

单线程

使用 parse 来模拟解析函数

index.js

1
2
3
4
5
6
function parse (time) {
const start = new Date();
while(new Date() - start < time) {}
return 'DIYgod'
}
console.log(parse(1000));

此时页面会卡死 1s,然后输出一个 ‘DIYgod’。

使用 Web Workers

index.js

1
2
3
4
5
const wk = new Worker('worker.js');
wk.postMessage(1000);
wk.addEventListener('message', (e) => {
console.log(e.data);
});

worker.js

1
2
3
4
5
6
7
8
9
function parse (time) {
const start = new Date();
while(new Date() - start < time) {}
return 'DIYgod';
}

onmessage = function (e) {
postMessage(parse(e.data));
}

这是 Web Workers 的一个最基础用法,index.js 把 1000 传给 worker.js,worker.js 在后台解析 1000 ms,再把结果 ‘DIYgod’ 传回 index.js,这样解析就不会再占用 js 主线程,避免了页面卡死。

内嵌 Worker

上一步我们加载了两个 js 文件,index.js 和 worker.js,在 HTML 里引用 index.js,然后 index.js 会加载 worker.js,那么不想创建单独的 Worker 文件怎么办呢?

index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const workerBlob = new Blob([`function parse (time) {
const start = new Date();
while(new Date() - start < time) {}
return 'DIYgod';
}

onmessage = function (e) {
postMessage(parse(e.data));
}`], { type: 'application/javascript' });
const workerURL = URL.createObjectURL(workerBlob);

const wk = new Worker(workerURL);
wk.postMessage(1000);
wk.addEventListener('message', (e) => {
console.log(e.data);
});

URL.createObjectURL(blob) 会创建一个 DOMString,它包含一个表示 blob 的 URL。

打开控制台的 Network 标签页,你会看到浏览器加载了一个形如 blob:http://example.com/16215a1e-21d4-450c-b441-070e1981b69d 的奇怪链接的 js 文件,这个 js 文件的内容正是我们传给 workerBlob 的字符串内容。

这个 URL 是唯一的,且它的生命周期和创建它的窗口中的 document 绑定,只要页面存在,该网址就会一直有效。

使用 webpack worker-loader

上一步中我们把 js 代码放在了字符串里,它不能拆分模块,也不利于后期维护,如果项目正在使用 webpack,安装 worker-loader 可以解决这个问题。

index.js

1
2
3
4
5
6
7
import WK from 'worker-loader?inline=true&fallback=false!./worker.js';

const wk = new WK();
wk.postMessage(1000);
wk.addEventListener('message', (e) => {
console.log(e.data);
});

worker.js

1
2
3
4
5
import Parse from './parse.js';

self.addEventListener('message', (e) => {
self.postMessage(Parse(e.data));
});

parse.js

1
2
3
4
5
6
7
function Parse (time) {
const start = new Date();
while(new Date() - start < time) {}
return 'DIYgod';
}

export default Parse;

只需要使用 worker-loader 引用 worker.js 模块,剩下的 worker-loader 会帮我们自动处理,最后编译的结果类似我们上一步的代码。

对比不使用 Web Workers 时:

index.js

1
2
3
import Parse from './parse.js';

console.log(Parse(1000));

parse.js(不变)

1
2
3
4
5
6
function Parse (time) {
const start = new Date();
while(new Date() - start < time) {}
return 'DIYgod';
}
export default Parse;

这样不用修改原有的解析模块,非侵入式,只需要加个 worker.js 中转模块,再改下调用方法即可,维护起来也很方便。

性能

如果我把一个计算放入 4 个 Worker,那么这个计算会快 4 倍?

不,它不仅不会快 4 倍,而且会变得更慢。

Web Workers 不是为了缩短计算时间,而是为了避免 UI 线程冻结。创建线程、线程调度、传输数据等行为会导致计算变得比单线程稍微更慢一点。

我记录了开头那个视频在不同 Worker 数量下解析 100 条弹幕的时间,7 次记录取平均值:

Worker 数量 0 1 2 3 4 5 6 10
平均时间(ms) 6085 8216 6310 6388 6483 6317 6475 7233

不使用 Worker 的解析速度最快,1 个 Worker 的速度比其他明显更慢,2 3 4 5 6 个 Worker 速度没有明显差异,但 Worker 数量一直增加速度又会逐渐变慢。

另外又测试了弹幕比较少的视频,结果是 1 2 3 4 5 个 Worker 的速度都差不多。

最后不靠谱地决定使用 2 个 Worker 进行解析。

优化结果妙不可言,不需要等待解析完成才能进行其他操作,也可以一边播放视频一边解析,区别只是播放到没解析好的弹幕不会显示,解析完成才会显示。

分享到:
Disqus 加载中...

如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理