requestIdleCallback和requestAnimationFrame詳解


頁面流暢與 FPS

頁面是一幀一幀繪制出來的,當每秒繪制的幀數(FPS)達到 60 時,頁面是流暢的,小於這個值時,用戶會感覺到卡頓。

1s 60幀,所以每一幀分到的時間是 1000/60 ≈ 16 ms。所以我們書寫代碼時力求不讓一幀的工作量超過 16ms。

Frame

那么瀏覽器每一幀都需要完成哪些工作?

 
image

通過上圖可看到,一幀內需要完成如下六個步驟的任務:

  • 處理用戶的交互
  • JS 解析執行
  • 幀開始。窗口尺寸變更,頁面滾去等的處理
  • requestAnimationFrame(rAF)
  • 布局
  • 繪制

requestIdleCallback

上面六個步驟完成后沒超過 16 ms,說明時間有富余,此時就會執行 requestIdleCallback 里注冊的任務。

 
image

從上圖也可看出,和 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 方法的時候,執行動畫,我們可能使用 setTimeoutsetInterval 來觸發視覺變化;但是這種做法的問題是:回調函數執行的時間是不固定的,可能剛好就在末尾,或者直接就不執行了,經常會引起丟幀而導致頁面卡頓。

 
image

歸根到底發生上面這個問題的原因在於時機,也就是瀏覽器要知道何時對回調函數進行響應。setTimeoutsetInterval 是使用定時器來觸發回調函數的,而定時器並無法保證能夠准確無誤的執行,有許多因素會影響它的運行時機,比如說:當有同步代碼執行時,會先等同步代碼執行完畢,異步隊列中沒有其他任務,才會輪到自己執行。並且,我們知道每一次重新渲染的最佳時間大約是 16.6 ms,如果定時器的時間間隔過短,就會造成 過度渲染,增加開銷;過長又會延遲渲染,使動畫不流暢。

requestAnimationFrame 方法不同與 setTimeoutsetInterval,它是由系統來決定回調函數的執行時機的,會請求瀏覽器在下一次重新渲染之前執行回調函數。無論設備的刷新率是多少,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
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。

喜歡這篇文章?歡迎打賞~~

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM