頁面流暢與 FPS
頁面是一幀一幀繪制出來的,當每秒繪制的幀數(FPS)達到 60 時,頁面是流暢的,小於這個值時,用戶會感覺到卡頓。
1s 60幀,所以每一幀分到的時間是 1000/60 ≈ 16 ms。所以我們書寫代碼時力求不讓一幀的工作量超過 16ms。
Frame
那么瀏覽器每一幀都需要完成哪些工作?
通過上圖可看到,一幀內需要完成如下六個步驟的任務:
- 處理用戶的交互
- JS 解析執行
- 幀開始。窗口尺寸變更,頁面滾去等的處理
- rAF
- 布局
- 繪制
requestIdleCallback
上面六個步驟完成后沒超過 16 ms,說明時間有富余,此時就會執行 requestIdleCallback
里注冊的任務。
requestIdleCallback
在瀏覽器一幀內的位置示意
從上圖也可看出,和 requestAnimationFrame
每一幀必定會執行不同,requestIdleCallback
是撿瀏覽器空閑來執行任務。
如此一來,假如瀏覽器一直處於非常忙碌的狀態,requestIdleCallback
注冊的任務有可能永遠不會執行。此時可通過設置 timeout
(見下面 API 介紹)來保證執行。
API
var handle = window.requestIdleCallback(callback[, options])
callback: ()
:回調即空閑時需要執行的任務,接收一個IdleDeadline
對象作為入參。其中IdleDeadline
對象包含:didTimeout
,布爾值,表示任務是否超時,結合timeRemaining
使用。timeRemaining()
,表示當前幀剩余的時間,也可理解為留給任務的時間還有多少。
options
:目前 options 只有一個參數timeout
。表示超過這個時間后,如果任務還沒執行,則強制執行,不必等待空閑。
示例
requestIdleCallback(myNonEssentialWork, { timeout: 2000 });
function myNonEssentialWork (deadline) {
// 如果幀內有富余的時間,或者超時
while ((deadline.timeRemaining() > 0 || deadline.didTimeout) &&
tasks.length > 0)
doWorkIfNeeded();
if (tasks.length > 0)
requestIdleCallback(myNonEssentialWork);
}
超時的情況,其實就是瀏覽器很忙,沒有空閑時間,此時會等待指定的 timeout
那么久再執行,通過入參 dealine
拿到的 didTmieout
會為 true
,同時 timeRemaining ()
返回的也是 0。超時的情況下如果選擇繼續執行的話,肯定會出現卡頓的,因為必然會將一幀的時間拉長。
cancelIdleCallback
與 setTimeout
類似,返回一個唯一 id,可通過 cancelIdleCallback
來取消任務。
總結
一些低優先級的任務可使用 requestIdleCallback
等瀏覽器不忙的時候來執行,同時因為時間有限,它所執行的任務應該盡量是能夠量化,細分的微任務(micro task)。
因為它發生在一幀的最后,此時頁面布局已經完成,所以不建議在 requestIdleCallback
里再操作 DOM,這樣會導致頁面再次重繪。DOM 操作建議在 rAF 中進行。同時,操作 DOM 所需要的耗時是不確定的,因為會導致重新計算布局和視圖的繪制,所以這類操作不具備可預測性。
Promise 也不建議在這里面進行,因為 Promise 的回調屬性 Event loop 中優先級較高的一種微任務,會在 requestIdleCallback
結束時立即執行,不管此時是否還有富余的時間,這樣有很大可能會讓一幀超過 16 ms。