這個視頻有 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 數量 | 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 進行解析。
優化結果妙不可言,不需要等待解析完成才能進行其他操作,也可以一邊播放視頻一邊解析,區別只是播放到沒解析好的彈幕不會顯示,解析完成才會顯示。