淺析requestIdleCallback


  我們都知道React 16實現了新的調度策略(Fiber),新的調度策略提到的異步、可中斷,其實就是基於瀏覽器的 requestIdleCallback 和 requestAnimationFrame 這兩個API。

  requestAnimationFrame就不多說可以看剛剛這篇博客:淺析requestAnimationFrame讓你更加了解動畫

  那么什么是requestIdleCallback?當關注用戶體驗,不希望因為一些不重要的任務(如統計上報)導致用戶感覺到卡頓的話,就應該考慮使用requestIdleCallback。

  因為requestIdleCallback回調的執行的前提條件是當前瀏覽器處於空閑狀態

requestIdleCallback will schedule work when there is free time at the end of a frame, or when the user is inactive.

  我們先來了解一些相關背景知識。

一、頁面流暢與 FPS

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

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

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

  通過上圖可看到,一幀內需要完成如下六個步驟的任務:
  • 處理用戶的交互,如點擊、觸碰、滾動等事件
  • JS 解析執行
  • 幀開始。窗口尺寸變更,頁面滾動等的處理
  • requestAnimationFrame(rAF)
  • 布局
  • 繪制

二、為什么需要 requestIdleCallback ?

  在網頁中,有許多耗時但是卻又不能那么緊要的任務。它們和緊要的任務,比如對用戶的輸入作出及時響應的之類的任務,它們共享事件隊列。如果兩者發生沖突,用戶體驗會很糟糕。我們可以使用setTimout,對這些任務進行延遲處理。但是我們並不知道,setTimeout在執行回調時,是否是瀏覽器空閑的時候。

  而requestIdleCallback就解決了這個痛點,requestIdleCallback會在幀結束時並且有空閑時間,或者用戶不與網頁交互時,執行回調。

1、空閑時間

  requestIdleCallback 的callback會在瀏覽器的空閑時間運行,那么什么是空閑時間呢?

  空閑時間分兩種:

(1)第一種:

  如上圖:當我們在執行一段連續的動畫的時候,第一幀已經渲染到屏幕上了,到第二幀開始渲染,這段時間內屬於空閑時間。這種空閑時間會非常的短暫,如果我們的屏幕是60hz(1s內屏幕刷新60次)的。那么空閑時間會小於16ms(1000ms / 16)。

(2)第二種:

  另外一種空閑時間,當用戶屬於空閑狀態(沒有與網頁進行任何交互),並且屏幕中也沒有動畫執行。此時空閑時間是無限長的。但是為了避免不可預測的事(用戶突然和網頁進行交互),空閑時間最大應該被限制在50ms以內。

為什么最大是50ms?人類對100ms內的響應會認為是瞬時的。將空閑時間限制在50ms以內,是為了避免,空閑時間內執行任務,從而導致了對用戶操作響應的阻塞,使用戶感到明顯的響應滯后。

  在空閑期間,callback的執行順序是以FIFO(先進先出)的順序。但是如果在空閑時間內依次執行callback時,有一個callback的執行時間,已經將空閑時間用完了,剩下的callback將會在下一次的空閑時間執行。

三、requestIdleCallback

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

  從上圖也可看出,和 requestAnimationFrame 每一幀必定會執行不同,requestIdleCallback 是撿瀏覽器空閑來執行任務。如此一來,假如瀏覽器一直處於非常忙碌的狀態,requestIdleCallback 注冊的任務有可能永遠不會執行。此時可通過設置 timeout (見下面 API 介紹)來保證執行。

1、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

2、代碼示例

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。超時的情況下如果選擇繼續執行的話,肯定會出現卡頓的,因為必然會將一幀的時間拉長。

3、cancelIdleCallback

  與 setTimeout 類似,返回一個唯一 id,可通過 cancelIdleCallback 來取消任務。

4、requestIdleCallback和requestAnimationFrame有什么區別?

  requestAnimationFrame的回調會在每一幀確定執行,屬於高優先級任務,而requestIdleCallback的回調則不一定,屬於低優先級任務。

  我們所看到的網頁,都是瀏覽器一幀一幀繪制出來的,通常認為FPS為60的時候是比較流暢的,而FPS為個位數的時候就屬於用戶可以感知到的卡頓了,那么在一幀里面瀏覽器都要做哪些事情呢?

  根據前面的圖例我們知道一幀包含了用戶的交互、js的執行、以及requestAnimationFrame的調用,布局計算以及頁面的重繪等工作。

  假如某一幀里面要執行的任務不多,在不到16ms(1000/60)的時間內就完成了上述任務的話,那么這一幀就會有一定的空閑時間,這段時間就恰好可以用來執行requestIdleCallback的回調。

  當程序棧為空頁面無需更新的時候,瀏覽器其實處於空閑狀態,這時候留給requestIdleCallback執行的時間就可以適當拉長,最長可達到50ms,以防出現不可預測的任務(用戶輸入)來臨時無法及時響應可能會引起用戶感知到的延遲。

  由於requestIdleCallback利用的是幀的空閑時間,所以就有可能出現瀏覽器一直處於繁忙狀態,導致回調一直無法執行,這其實也並不是我們期望的結果(如上報丟失),那么這種情況我們就需要在調用requestIdleCallback的時候傳入第二個配置參數timeout了?

  如果是因為timeout回調才得以執行的話,其實用戶就有可能會感覺到卡頓了,因為一幀的執行時間必然已經超過16ms了。

5、requestIdleCallback里面可以執行DOM修改操作嗎?

  強烈建議不要,從上面一幀的構成里面可以看到,requestIdleCallback回調的執行說明前面的工作(包括樣式變更以及布局計算)都已完成。如果我們在callback里面做DOM修改的話,之前所做的布局計算都會失效,而且如果下一幀里有獲取布局(如getBoundingClientRect、clientWidth)等操作的話,瀏覽器就不得不執行強制重排工作,這會極大的影響性能。

  另外由於修改dom操作的時間是不可預測的,因此很容易超出當前幀空閑時間的閾值,故而不推薦這么做。

  推薦的做法是在requestAnimationFrame里面做dom的修改,可以在requestIdleCallback里面構建Document Fragment,然后在下一幀的requestAnimationFrame里面應用Fragment。

  除了不推薦DOM修改操作外,Promise的resolve(reject)操作也不建議放在里面,因為Promise的回調會在idle的回調執行完成后立刻執行,會拉長當前幀的耗時,所以不推薦。推薦放在requestIdleCallback里面的應該是小塊的(microTask)並且可預測時間的任務。

6、requestIdleCallback的兼容情況

7、總結

(1)一些低優先級的任務可使用 requestIdleCallback 等瀏覽器不忙的時候來執行,同時因為時間有限,它所執行的任務應該盡量是能夠量化,細分的微任務(micro task)。

(2)不能在 requestIdleCallback 里進行dom操作。因為它發生在一幀的最后,此時頁面布局已經完成,所以不建議在 requestIdleCallback 里再操作 DOM,這樣會導致頁面再次重繪。

(3)DOM 操作建議在 rAF 中進行。同時,操作 DOM 所需要的耗時是不確定的,因為會導致重新計算布局和視圖的繪制,所以這類操作不具備可預測性。

(4)Promise 也不建議在這里面進行,因為 Promise 的回調屬性 Event loop 中優先級較高的一種微任務,會在 requestIdleCallback 結束時立即執行,不管此時是否還有富余的時間,這樣有很大可能會讓一幀超過 16 ms。

(5)推薦使用npm包request-idle-callback


免責聲明!

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



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