頁面流暢與 FPS
頁面是一幀一幀繪制出來的,當每秒繪制的幀數(FPS)達到 60 時,頁面是流暢的,小於這個值時,用戶會感覺到卡頓。
1s 60幀,所以每一幀分到的時間是 1000/60 ≈ 16 ms。所以我們書寫代碼時力求不讓一幀的工作量超過 16ms。
Frame
那么瀏覽器每一幀都需要完成哪些工作?

通過上圖可看到,一幀內需要完成如下六個步驟的任務:
- 處理用戶的交互
- JS 解析執行
- 幀開始。窗口尺寸變更,頁面滾去等的處理
- requestAnimationFrame(rAF)
- 布局
- 繪制
requestIdleCallback
上面六個步驟完成后沒超過 16 ms,說明時間有富余,此時就會執行 requestIdleCallback
里注冊的任務。

從上圖也可看出,和 requestAnimationFrame
每一幀必定會執行不同,requestIdleCallback
是撿瀏覽器空閑來執行任務。
如此一來,假如瀏覽器一直處於非常忙碌的狀態,requestIdleCallback
注冊的任務有可能永遠不會執行。此時可通過設置 timeout
(見下面 API 介紹)來保證執行。
API
var handle = window.requestIdleCallback(callback[, options])
- callback:回調,即空閑時需要執行的任務,該回調函數接收一個
IdleDeadline
對象作為入參。其中IdleDeadline
對象包含:didTimeout
,布爾值,表示任務是否超時,結合timeRemaining
使用。timeRemaining()
,表示當前幀剩余的時間,也可理解為留給任務的時間還有多少。
- options:目前 options 只有一個參數
timeout
。表示超過這個時間后,如果任務還沒執行,則強制執行,不必等待空閑。
IdleDeadline
對象參考MDN:https://developer.mozilla.org/zh-CN/docs/Web/API/IdleDeadline
示例
requestIdleCallback(myNonEssentialWork, { timeout: 2000 }); // 任務隊列 const tasks = [ () => { console.log("第一個任務"); }, () => { console.log("第二個任務"); }, () => { console.log("第三個任務"); }, ]; function myNonEssentialWork (deadline) { // 如果幀內有富余的時間,或者超時 while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && tasks.length > 0) { work(); } if (tasks.length > 0) requestIdleCallback(myNonEssentialWork); } function work () { tasks.shift()(); console.log('執行任務'); }
超時的情況,其實就是瀏覽器很忙,沒有空閑時間,此時會等待指定的 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。
額外補充一下window.requestAnimationFrame
在沒有 requestAnimationFrame
方法的時候,執行動畫,我們可能使用 setTimeout
或 setInterval
來觸發視覺變化;但是這種做法的問題是:回調函數執行的時間是不固定的,可能剛好就在末尾,或者直接就不執行了,經常會引起丟幀而導致頁面卡頓。

歸根到底發生上面這個問題的原因在於時機,也就是瀏覽器要知道何時對回調函數進行響應。setTimeout
或 setInterval
是使用定時器來觸發回調函數的,而定時器並無法保證能夠准確無誤的執行,有許多因素會影響它的運行時機,比如說:當有同步代碼執行時,會先等同步代碼執行完畢,異步隊列中沒有其他任務,才會輪到自己執行。並且,我們知道每一次重新渲染的最佳時間大約是 16.6 ms,如果定時器的時間間隔過短,就會造成 過度渲染,增加開銷;過長又會延遲渲染,使動畫不流暢。
requestAnimationFrame
方法不同與 setTimeout
或 setInterval
,它是由系統來決定回調函數的執行時機的,會請求瀏覽器在下一次重新渲染之前執行回調函數。無論設備的刷新率是多少,requestAnimationFrame
的時間間隔都會緊跟屏幕刷新一次所需要的時間;例如某一設備的刷新率是 75 Hz,那這時的時間間隔就是 13.3 ms(1 秒 / 75 次)。需要注意的是這個方法雖然能夠保證回調函數在每一幀內只渲染一次,但是如果這一幀有太多任務執行,還是會造成卡頓的;因此它只能保證重新渲染的時間間隔最短是屏幕的刷新時間。
requestAnimationFrame
方法的具體說明可以看 MDN 的相關文檔,下面通過一個網頁動畫的示例來了解一下如何使用。
let offsetTop = 0; const div = document.querySelector(".div"); const run = () => { div.style.transform = `translate3d(0, ${offsetTop += 10}px, 0)`; window.requestAnimationFrame(run); }; run();
如果想要實現動畫效果,每一次執行回調函數,必須要再次調用 requestAnimationFrame
方法;與 setTimeout
實現動畫效果的方式是一樣的,只不過不需要設置時間間隔。
參考文章
作者:DC_er
鏈接:https://www.jianshu.com/p/2771cb695c81
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
喜歡這篇文章?歡迎打賞~~