差點被當塑膠的 Web API / RequestIdleCallback
時間管力大師就是要忙裡偷閒
各位應該知道 JavaScript 是單執行緒(單線程)的程式語言,也就是一次只能處理一件事情。這樣的特性會使得事件的執行必定有個先後順序,這時候就會希望重要的事情能夠排序在前面,剩下比較不重要的任務等空閒時再處理即可,這時候就可以靠 RequestIdleCallback 來幫助我們。
RequestIdleCallback
RequestIdleCallback 會在瀏覽器「每一幀」中剩下的空閒裡來執行當中的 Callback。
我們之前在介紹 RequestAnimationFrame 時有提過「幀數(FPS)」的概念,也就是「一秒鐘內能夠更新多少幀」,假如在一秒內能夠更新 60 幀,則 FPS 為 60,每一幀的時間約為 16.7 ms(毫秒)。
對於瀏覽器來說每一次「重繪(Repaint)」就是「一幀」,而這一幀要花多少時間就要看當下的網路或硬體狀況而定了。在這每一幀中,瀏覽器都有可能正在執行任務,若這個任務完成時,當下那一幀還沒結束時,就會有一個短暫的空閒時間。
以 60FPS 為例,每一幀的空閒時間必定小於等於 16.7 ms。
而只要有這個空閒時間 RequestIdleCallback 就會去執行當中的 Callback,來完成那些我們覺得不重要的任務,換句話說,如果瀏覽器一直處於繁忙狀態的話,那該任務就會一直無法執行。
# Window.requestIdleCallback
requestIdleCallback
有兩個參數要傳入:
- callback: 需要在空閒時間(Idle)執行的函示。
- timeout: 這是一個可選參數,你可以設定一個時間來強制執行
callback
,以避免瀏覽器因為持續繁忙的忽略(單位:毫秒)。
大部分情況不建議使用
timeout
,因為會使用requestIdleCallback
就代表不想影響主線程的任務進行 。
const handlerId = requestIdleCallback(function () {
//..做些不住要的事
}, 500);
cancelIdleCallback(handlerId); // 取消requestIdleCallback
# IdleDeadline
而我們傳入的 Callback Function 會被丟進一個由 requestIdleCallback
提供的參數,該參數通常取名為 deadline
,並且有兩個屬性可以使用:
- didTimeout: 這是一個唯讀屬性,以布林值來表示 Callback 是否是因為
timeout
被觸發的。 - timeRemaining: 它是一個 method,執行後會傳傳一個毫秒數,用來表示當下這一幀的剩餘時間。
requestIdleCallback(function (deadline) {
// 如果你在 requestIdleCallback 中沒有傳入 timeout 參數,didTimeout 必定為 false
console.log(deadline.didTimeout);
console.log(deadline.timeRemaining());
}, 500);
# 實際測試
由於 JavaScript 是單執行緒,所以要是我今天進行了一個需要耗費大量時間的任務,那使用者的 UI 操作其實也會受到影響。
就像下面這個範例中,在 count
被函式 add
加到 1000000 以前,你不管怎麼敲擊鍵盤,keydown
事件都不會被觸發,因為瀏覽器正在忙著算數:
window.addEventListener("keydown", function () {
console.log("Hey !!!!!!!!!");
});
let count = 0;
add();
function add() {
if (count < 10000) {
console.log(count++);
add();
}
}
但是我們只要用 requestIdleCallback
來改寫一下,那狀況就不一樣了,因為這時候 add
這項任務的優先度會往後排,所以當我按下鍵盤時,瀏覽器會先處理 keydown
事件,等到閒置下來後才會繼續進行。
window.addEventListener("keydown", function () {
console.log("Hey !!!!!!!!!");
});
let count = 0;
requestIdleCallback(add);
function add(deadline) {
if (deadline.timeRemaining() > 0) {
if (count < 10000) {
console.log(count++);
requestIdleCallback(add);
}
}
}
# 使用情境
在了解 RequestIdleCallback 的效果後,我第一個想到的實際應用會是 LazyLoad,想像以下,如果我們有個網頁,當中有幾十甚至幾百張的高畫質圖片需要顯示,可想而知瀏覽器的負擔會相當的大,非常有可能會影響頁面的效能與任務執行,但如果們我利用 requestIdleCallback
來處理,就可以在不影響主執行緒的情況下載入圖片。
const images = [
"https://img/001.png",
"https://img/002.png",
//.....
"https://img/099.png",
"https://img/100.png",
];
requestIdleCallback(loadImage);
function loadImage(deadline) {
if (deadline.timeRemaining() > 0) {
if (images.length) {
const imgSrc = images.shift();
const img = new Image(250, 150);
img.onload = document.body.appendChild(img);
img.src = imgSrc;
requestIdleCallback(loadImage);
}
}
}
不曉得使用過 React 的朋友有沒有了解過 React Fiber 呢?其實它的原理就和 RequestIdleCallback 一樣,將大量沒那麼優先的工作拆成許多小片段,在瑣碎的時間裡慢慢完成,也因為這樣的機制,使得我們可以去中斷它,將一些突發的重要任務(例如使用者的 UI 事件)插在這些小片段中,宛如有另一條執行緒一般。
- 此篇文章為「iT 邦幫忙鐵人賽」參賽文章,同步發表於 iT 邦幫忙 -