# 探索 Browser API (上)
# 前言
最近這幾年許多 Browser 原生的 API 陸續被實作出來,試圖解決前端遇到的問題,這個系列試圖介紹一些比較有趣或是比較被廣泛支援的 API,以後遇到類似的問題或許可以試試看用原生的 API 解決。
# reqeustIdleCallback
許多網頁當中都有各式各樣的 script
需要執行,當然也會有優先程度,像是比較重要的:渲染 UI,註冊相關的互動事件,呼叫 API 拿取資料等等是高優先的任務,而像是比較不重要的任務有:Analytic 的腳本、lazy loading、初始化比較不重要的事件。
# requestIdleCallback 簡介
requestIdleCallback
會在 frame 的最後執行,但並不是每一個 frame 都保證會執行 requestIdleCallback
。這個原因很簡單,我們無法保證每一個 frame 結束時我們還有時間,所以並不能保證 requestIdleCallback
的執行時間。
仔細一看,會覺得 requestIdleCallback 有點像是 context switch 的感覺,你可以在 frame 與 frame 之間完成一些工作,中斷一下,然後再繼續執行。
# 怎樣才算 Idle?
怎樣才能知道瀏覽器處於 Idle 的狀態?這是一個相當複雜的問題,瀏覽器幫我們排程了一連串的任務,解析 HTML, CSS, Javascript、渲染 UI、API calls、抓取圖片並且解析、GPU 加速等等,要知道什麼時候閒置勢必得了解瀏覽器的排程工作。不過很幸運地是,requestIdleCallback
幫我們解決了這個問題。
# requestIdleCallback(fn, {timeout})
另外第二個參數 options 裡頭有個 timeout
選項,如果在 timeout 期間瀏覽器都還沒有呼叫的話,你可以用這個 timeout 讓瀏覽器停下手邊的工作強制呼叫。
這不是一個適當的使用方式,因為我們使用 idle 的原因就是因為這個任務是不重要的,沒必要為了它而打斷手邊的工作。瀏覽器提供了這份彈性給我們,有時你還是會希望事件在某個時間點以內觸發。
# cancelIdleCallback(id)
對應到 requestIdleCallback()
會回傳一個 id,我們也可以呼叫 cancelIdleCallback(id)
來取消不需要的 idle callback。
# requestIdleCallback 會不會被中斷?
deadline
參數表示這一個 frame 當中,你有多少時間可以完成這個任務,傳入的 callback 可以取得這個參數,根據官方的說法,就算超過了這個時間,瀏覽器也不會強制中斷你的任務,只是希望你能夠在 deadline
完成,讓使用者有最佳體驗。
deadline.timeRemaining()
回傳當前的可利用的時間還有多少。
# 在 requestIdleCallback 執行 DOM 操作會怎樣?
不妨設想一下,前文有提到,requestIdleCallback
會在 frame 的最後才執行,表示瀏覽器已經做完 recaculate, layout, paint 的工作了,在這時修改 DOM 的話,等於強迫瀏覽器又要再一次做 recaculate style, layout, paint 的排程。
# 在 requestIdleCallback 裡呼叫 requestIdleCallback 會怎樣?
在 requestIdleCallback
裡頭呼叫 requestIdleCallback
是合法的。不過這個 callback 會被安排到下一個 frame。(實際上並不一定是下一個 frame,視瀏覽器的排程而定)
# Example, please!
假設我們有幾個使用者,當滑鼠移至大頭照上方時,會出現個人簡介。為了善用 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
是否有值,開發上反而變得比較麻煩一些,一次抓取多位使用者的資料還比較省事一些,不過這完全根據需求與使用場景而定。
# Analytic
另外一個常見的場景是做追蹤,例如追蹤使用者點擊按鈕,點擊播放,觀看時間等等。我們可以先蒐集事件,再利用 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
,甚至幫你的任務標註優先度以確保執行順序等等,都是可以優化的地方。