banner
DIYgod

Hi, DIYgod

写代码是热爱,写到世界充满爱!
github
twitter
follow
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 進行解析。

優化結果妙不可言,不需要等待解析完成才能進行其他操作,也可以一邊播放視頻一邊解析,區別只是播放到沒解析好的彈幕不會顯示,解析完成才會顯示。

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。