提示
希望你能瞭解什麼是 Event Loop(事件迴圈),以及對 Web Worker 有所瞭解,以便更容易吸收
什麼是長任務
W3C 效能組規定:執行時長大於 50ms 的任務,定義為長任務
那麼我們如何對長任務進行優化?
setTimeout
js 是單執行緒語言,它的作用主要用於操作DOM。 js執行也非常簡單(從上往下執行),但 js 裡也有非同步方法,比如 xhr 、 setTimeout 等等。
setTimeout 的作用是:將當前任務推入任務佇列,當主執行緒同步的程式碼執行完成後判斷 setTimeout 設定的時間有沒有到,如果到了將任務推出佇列執行。
下面是 Event Loop 示例圖:
由於長任務執行時間長,會阻塞主執行緒,使用者能感覺到頁面卡頓,所以我們經常會採用 setTimeout 把長任務推入任務佇列,等到同步程式碼執行完成後再處理長任務:
const t = setTimeout(() =>{
clearTimeout(t);
// 長任務
},0)
複製程式碼
注意點
由於前端處理長任務的場景並不多,一般由服務端處理完後給到前端,所以 setTimeout 能解決大部分的長任務問題,但是該方法不能用於時間過長的任務,比如一個任務需要秒級時長,使用者仍然會感覺頁面卡死。
下面這個場景也會導致卡頓:
#box {
width: 100px;
height: 100px;
background: green;
}
<div id="box"></div>
// 動畫 大概需要2s跑完
var start = null;
var element = document.getElementById('box');
element.style.position = 'absolute';
function step(timestamp) {
if (!start) start = timestamp;
var progress = timestamp - start;
element.style.left = Math.min(progress / 10, 200) + 'px';
if (progress < 2000) {
window.requestAnimationFrame(step);
}
}
window.requestAnimationFrame(step);
setTimeout(function() {
// 500ms的長任務
var now = performance.now();
while (now + 500 > performance.now()) {}
}, 100);
複製程式碼
上面程式碼中使用 setTimeout 來處理長任務,但是頁面中有個需要 2s 跑完的動畫效果,當執行到 setTimeout 時,動畫仍在執行,這時在佇列裡的長任務被推出並執行導致主執行緒阻塞,動畫出現卡頓,這時最好不阻塞主執行緒,那麼使用 Web Worker 是最合適的(Web Worker 在下文介紹)
使用時間分片
時間分片並不是某個 api,而是一種技術方案,它可以把長任務分割成若干個小任務執行,並在執行小任務的間隔中把主執行緒的控制權讓出來,這樣就不會導致UI卡頓。
React 的 Fiber 技術核心思想也是時間分片,Vue 2.x 也用了時間分片,只不過是以元件為單位來實施分片操作,由於收益不高 Vue 3 把時間分片移除了。
演示
為了好理解,先寫段長任務程式碼,將主執行緒阻塞 1秒鐘:
const start = performance.now();
let count = 0;
while (performance.now() - start < 1000) {}
console.log('done!');
複製程式碼
該段指令碼霸佔主線產長達 1s 的時間 我們可以封裝一個 ts 方法,讓這個長任務被分割成多個小任務執行:
ts(function* (){
const start = performance.now();
let count = 0;
while (performance.now() - start < 1000) {
yield;
}
console.log('done!');
})()
複製程式碼
先看看效果吧:
從圖裡看到,一個長任務被切成了諾幹個小任務,在每個小任務間隔中把主執行緒的控制權交出來,這樣就不會導致頁面卡頓
基於 Generator 函式實現時間分片方法
基於 Generator 函式的執行特性,我們很容易使用它來實現一個時間分片函式:
function ts(gen) {
if (typeof gen === 'function') gen = gen();
if (!gen || typeof gen.next !== 'function') return;
return function next() {
const start = performance.now();
const res = null;
do {
res = gen.next();
} while (!res.done && performance.now() - start < 25);
if (res.done) return;
setTimeout(next);
};
}
複製程式碼
上面程式碼中,做了一個 do while:如果當前任務執行時間低於 25ms 則多個任務一起執行,否則作為一個任務執行,一直到 res.done = true 為止。
程式碼核心思想:通過 yield 關鍵字可以將任務暫停執行,並讓出主執行緒的控制權;通過setTimeout
將未完成的任務重新放在任務佇列中執行
使用 Web Worker 優化
文章開頭提過 js 是單執行緒,但瀏覽器不是。 Web Worker 允許我們在後臺建立獨立於主執行緒的其他執行緒,所以我們可以把一些費時費力的長任務交給 Web Worker。
使用
由於共享執行緒瀏覽器支援情況較差,本章我們只介紹專用執行緒。
我們建立一個資料夾,並在裡面建立 index.html 和 worker.js 目錄如下:
.
├── index.html
└── worker.js
複製程式碼
index.html 程式碼:
<input type="text" id="ipt" value="" />
<div id="result"></div>
<script>
const ipt = document.querySelector('#ipt');
const worker = new Worker('worker.js');
ipt.onchange = function() {
// 通過postMessage傳送訊息
worker.postMessage({ number: this.value });
};
// 通過onmessage接收訊息
worker.onmessage = function(e) {
document.querySelector('#result').innerHTML = e.data;
};
</script>
複製程式碼
worker.js 程式碼:
// self 類似主執行緒中的 window
self.onmessage = function(e) {
self.postMessage(e.data.number * 2);
};
複製程式碼
總結
- 對於大多數場景 setTimeout 夠用,但要注意使用場景
- 需要更長時間來執行的任務,可以使用時間分片或者交給 Web Worker 來解決