最近這幾年許多 Browser 原生的 API 陸續被實作出來,試圖解決前端遇到的問題,這個系列試圖介紹一些比較有趣或是比較被廣泛支援的 API,以後遇到類似的問題或許可以試試看用原生的 API 解決。
許多網頁當中都有各式各樣的 script
需要執行,當然也會有優先程度,像是比較重要的:渲染 UI,註冊相關的互動事件,呼叫 API 拿取資料等等是高優先的任務,而像是比較不重要的任務有:Analytic 的腳本、lazy loading、初始化比較不重要的事件。
requestIdleCallback
會在 frame 的最後執行,但並不是每一個 frame 都保證會執行 requestIdleCallback
。這個原因很簡單,我們無法保證每一個 frame 結束時我們還有時間,所以並不能保證 requestIdleCallback
的執行時間。
仔細一看,會覺得 requestIdleCallback 有點像是 context switch 的感覺,你可以在 frame 與 frame 之間完成一些工作,中斷一下,然後再繼續執行。
怎樣才能知道瀏覽器處於 Idle 的狀態?這是一個相當複雜的問題,瀏覽器幫我們排程了一連串的任務,解析 HTML, CSS, Javascript、渲染 UI、API calls、抓取圖片並且解析、GPU 加速等等,要知道什麼時候閒置勢必得了解瀏覽器的排程工作。不過很幸運地是,requestIdleCallback
幫我們解決了這個問題。
const myWork = (deadline) => {
console.log('not important job.');
while (deadline.timeRemaining() > 0) {
// sending logging
// fetch non-essential data.
// use your imaginary
}
abortMyJob();
};
另外第二個參數 options 裡頭有個 timeout
選項,如果在 timeout 期間瀏覽器都還沒有呼叫的話,你可以用這個 timeout 讓瀏覽器停下手邊的工作強制呼叫。
這不是一個適當的使用方式,因為我們使用 idle 的原因就是因為這個任務是不重要的,沒必要為了它而打斷手邊的工作。瀏覽器提供了這份彈性給我們,有時你還是會希望事件在某個時間點以內觸發。
對應到 requestIdleCallback()
會回傳一個 id,我們也可以呼叫 cancelIdleCallback(id)
來取消不需要的 idle callback。
deadline
參數表示這一個 frame 當中,你有多少時間可以完成這個任務,傳入的 callback 可以取得這個參數,根據官方的說法,就算超過了這個時間,瀏覽器也不會強制中斷你的任務,只是希望你能夠在 deadline
完成,讓使用者有最佳體驗。
deadline.timeRemaining()
回傳當前的可利用的時間還有多少。
不妨設想一下,前文有提到,requestIdleCallback
會在 frame 的最後才執行,表示瀏覽器已經做完 recaculate, layout, paint 的工作了,在這時修改 DOM 的話,等於強迫瀏覽器又要再一次做 recaculate style, layout, paint 的排程。
在 requestIdleCallback
裡頭呼叫 requestIdleCallback
是合法的。不過這個 callback 會被安排到下一個 frame。(實際上並不一定是下一個 frame,視瀏覽器的排程而定)
假設我們有幾個使用者,當滑鼠移至大頭照上方時,會出現個人簡介。為了善用 idle 期間,我們可以在 requestIdleCallback
就先偷偷抓取需要的 API,如果都還沒有 fetch 過資料,再呼叫 API 去拿資料。
function fetchUser(name) {
const users = {
kalan: "food, coffee, life",
jack: "woman, coffee, life",
};
return Promise.resolve(users[name]);
}
const userIntro = {};
const queue = [
{name: 'kalan', fetched: false },
{ name: 'jack', fetched: false },
];
requestIdleCallback((deadline) => {
while (deadline.timeRemaining() > 0) {
let q = queue.pop();
fetchUser(q.name).then(user => {
if (deadline.timeRemaining() > 0) {
userIntro[user.name] = user;
q.fetched = true;
}
});
}
}, 500);
avatar.addEventListener('mouseover', (e) => {
const name = e.target.getAttribute('data-name');
if (userIntro[name]) {
// show intro
} else {
fetchUser(name).then(user => showInfo(user));
}
});
這個例子當中,我們利用 requestIdleCallback
先去抓取 user 的資料存起來,之後若使用者點擊大頭照就可以直接秀出來。如果還沒有抓到的話再重新 fetchUser
一次。
為了示範 requestIdleCallback
的效果,這個例子看起來挺麻煩的,不但需要維護一組 queue 來判斷是否已經 fetch
過,如果 mouseover
觸發了,還要另外判斷一次 userIntro
是否有值,開發上反而變得比較麻煩一些,一次抓取多位使用者的資料還比較省事一些,不過這完全根據需求與使用場景而定。
另外一個常見的場景是做追蹤,例如追蹤使用者點擊按鈕,點擊播放,觀看時間等等。我們可以先蒐集事件,再利用 requestIdleCallback
的方式一次送出。像是:
const btns = btns.forEach(btn => // buttons you want to track.
btn.addEventListener('click', e => {
// do other interactions...
//...
putIntoQueue({
type: 'click'
// collect your data
}));
schedule();
});
function schedule() {
requestIdleCallback(
deadline => {
while (deadline > 0) {
const event = queues.pop();
send(event);
}
},
{ timeout: 1000 }
);
}
這裡加上了 timeout
來確保瀏覽器會呼叫 schedule 函數。
以上這些例子只是為了示範方便,實際上要處理的問題很多,例如如何管理 queue、如何管理 timeout
,甚至幫你的任務標註優先度以確保執行順序等等,都是可以優化的地方。