banner
DIYgod

Hi, DIYgod

写代码是热爱,写到世界充满爱!
github
twitter
bilibili
telegram
email
steam
playstation
nintendo switch

Web Workers 初体验

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

单线程#

使用 parse 来模拟解析函数

index.js

function parse (time) {
    const start = new Date();
    while(new Date() - start < time) {}
    return 'DIYgod'
}
console.log(parse(1000));

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

使用 Web Workers#

index.js

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

worker.js

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

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

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

import Parse from './parse.js';

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

parse.js

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

import Parse from './parse.js';

console.log(Parse(1000));

parse.js(不变)

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 数量012345610
平均时间 (ms)60858216631063886483631764757233

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

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

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

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

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。