【譯】使用requestIdleCallback


原文地址:http://galen-yip.com/2015/10/07/%E3%80%90%E8%AF%91%E3%80%91%E4%BD%BF%E7%94%A8requestIdleCallback

英文原文:https://developers.google.com/web/updates/2015/08/27/using-requestidlecallback(備好梯子)

如有不當之處,還請指正。

 

概覽:

requestIdleCallback是一個當瀏覽器處於閑置狀態時,調度工作的新的性能相關的API

 

正文:

  如今,大多數的站點和app都需要執行很多的js腳本。你的js通常需要盡可能快地執行,而且,你又不希望通過獲取用戶行為的方式來達成目的。如果當用戶滾動頁面的時候,你的JS開始上報數據,或者當用戶點擊按鈕的時候,你往DOM中添加元素,你的web應用其實就已經變得遲鈍,導致很差的用戶體驗。

  現在有個好消息,一個新的API能夠幫助你: requestIdleCallback 。跟requestAnimationFrame一樣,requestAnimationFrame允許我們正確地安排動畫,同時最大限度地去提升到60fps。而requestIdleCallback則會在某一幀結束后的空閑時間或者用戶處於不活躍狀態時,處理我們的工作。這表明在不獲取用戶行為條件下,你能執行相關的工作。目前這個新的API在Chrome Canary(M46+)下可用(需要打開chrome://flags/#enable-experimental-web-platform-features 去開啟該功能),這樣你從今天開始先嘗試玩玩。但要記着,這個API是一個實驗性的功能,該規范仍在不斷變化,所以任何東西都可能隨時改變。

 

為什么我要使用requestIdleCallback?

  靠自己人工的安排不必要的工作是很困難的。比如,要弄清楚一幀剩余的時間,這顯然是不可能的,因為當requestAnimationFrame的回調完成后,還要進行樣式的計算,布局,渲染以及瀏覽器內部的工作等等。上面的話貌似還不能說明什么。為了確保用戶不以某種方式進行交互,你需要為各種交互行為添加監聽事件(scroll、touch、click),即使你並不需要這些功能,只有這樣才能絕對確保用戶沒有進行交互。另一方面,瀏覽器能夠確切地知道在一幀的結束時有多少的可用時間,如果用戶正在交互,通過使用requestIdleCallback這個API,允許我們盡可能高效地利用任何的空閑時間。

  接下來讓我們看看它的更多細節,且讓我們知道如果使用它。

 

檢查requestIdleCallback

  目前requestIdleCallback這個API仍處於初期,所以在使用它之前,你應該檢查它是否可用。

if ('requestIdleCallback' in window) {
  // Use requestIdleCallback to schedule work.
} else {
  // Do what you’d do today.
}

  現在,我們假設已經支持該API

 

使用requestIdleCallback

  調用requestIdleCallback跟調用requestAnimationFrame十分相似,它需要把回調函數作為第一個參數:

requestIdleCallback(myNonEssentialWork);

 

  當 myNonEssentialWork 被調用,會返回一個 deadline 對象,這個對象包含一個方法,該方法會返回一個數字表示你的工作還能執行多長時間:

function myNonEssentialWork (deadline) {
  while (deadline.timeRemaining() > 0)
    doWorkIfNeeded();
}

 

  調用 timeRemaining 這個方法能獲得最后的剩余時間,當 timeRemaining() 返回0,如果你仍有其他任務需要執行,你便可以執行另外的requestIdleCallback:

function myNonEssentialWork (deadline) {
  while (deadline.timeRemaining() > 0 && tasks.length > 0)
    doWorkIfNeeded();

  if (tasks.length > 0)
    requestIdleCallback(myNonEssentialWork);
}

 

確保你的方法已被調用

  當事件很多的時候,你會怎么做?你可能會擔心你的回調函數永遠不被執行。很好,盡管requestIdleCallback跟requestAnimationFrame很像,但它們也有不同,在於requestIdleCallback有一個可選的第二個參數:含有timeout屬性的對象。如果設置了timeout這個值,回調函數還沒被調用的話,則瀏覽器必須在設置的這個毫秒數時,去強制調用對應的回調函數。

// Wait at most two seconds before processing events.
requestIdleCallback(processPendingAnalyticsEvents, { timeout: 2000 });

  如果你的回調函數是因為設置的這個timeout而觸發的,你會注意到:

  • timeRemaining()會返回0
  • deadline對象的didTimeout屬性值是true

  

  如果你發現didTimeout是true,你的代碼可能會是這樣子的:

function myNonEssentialWork (deadline) {

  // Use any remaining time, or, if timed out, just run through the tasks.
  while ((deadline.timeRemaining() > 0 || deadline.didTimeout) &&
         tasks.length > 0)
    doWorkIfNeeded();

  if (tasks.length > 0)
    requestIdleCallback(myNonEssentialWork);
}

  因為設置timeout對你用戶導致的潛在破壞(這個操作會使你的app變得遲鈍且低質量),請小心地設置這個參數。所以,在這,就讓瀏覽器自己去決定什么時候觸發回調吧。

 

使用requestIdleCallback去上報數據

  讓我們試試用requestIdleCallback去上報數據。在這種情況下,我們可能希望去跟蹤一個事件,如“點擊導航欄菜單”。然而,因為通常他們是通過動畫展現在屏幕上的,我們希望避免立即發送事件到Google Analytics,因此我們將創建一個事件的數組來延遲上報,且在未來的某個時間點會發送出去。

var eventsToSend = [];

function onNavOpenClick () {

  // Animate the menu.
  menu.classList.add('open');

  // Store the event for later.
  eventsToSend.push(
    {
      category: 'button',
      action: 'click',
      label: 'nav',
      value: 'open'
    });

  schedulePendingEvents();
}

 

  現在我們使用requestIdleCallback來處理那些被掛起的事件。

function schedulePendingEvents() {

  // Only schedule the rIC if one has not already been set.
  if (isRequestIdleCallbackScheduled)
    return;

  isRequestIdleCallbackScheduled = true;

  if ('requestIdleCallback' in window) {
    // Wait at most two seconds before processing events.
    requestIdleCallback(processPendingAnalyticsEvents, { timeout: 2000 });
  } else {
    processPendingAnalyticsEvents();
  }
}

  上面代碼中,你可以看到我設置了2秒的超時,但取決於你的應用。因為對於上報的這些分析數據,設置一個timeout來確保數據在一個合理的時間范圍內被上報,而不是延遲到某個未知的時間點。這樣做才是合理且有意義的。

 

  最后我們來寫下requestIdleCallback執行的回調方法:

function processPendingAnalyticsEvents (deadline) {

  // Reset the boolean so future rICs can be set.
  isRequestIdleCallbackScheduled = false;

  // If there is no deadline, just run as long as necessary.
  // This will be the case if requestIdleCallback doesn’t exist.
  if (typeof deadline === 'undefined')
    deadline = { timeRemaining: function () { return Number.MAX_VALUE } };

  // Go for as long as there is time remaining and work to do.
  while (deadline.timeRemaining() > 0 && eventsToSend.length > 0) {
    var evt = eventsToSend.pop();

    ga('send', 'event',
        evt.category,
        evt.action,
        evt.label,
        evt.value);
  }

  // Check if there are more events still to send.
  if (eventsToSend.length > 0)
    schedulePendingEvents();
}

  這個例子中,我假設如果不支持requestIdleCallback,則立即上報數據。然而,對於一個在生產環境的應用,最好是用timeout延遲上報來確保不跟任何相互沖突。

 

 

使用requestIdleCallback改變dom

  requestIdleCallback可以幫助提高性能的另一個場景是,當你需要做一些非必要的dom改動,比如懶加載,滾動頁面時候不斷在尾部添加元素。讓我們看看requestIdleCallback事實上是如何插入一幀里的。

  對於瀏覽器,在給定的一幀內因為太忙而沒有去執行任何回調這是有可能的,所以你不應該期望在一幀的末尾有空閑的時間去做任何事。這一點就使得requestIdleCallback跟setImmediate不太像,setImmediate是在每一幀里都會執行。

  如果在某一幀的末尾,回調函數被觸發,它將被安排在當前幀被commit之后,這表示相應的樣式已經改動,同時更最重要的,布局已經重新計算。如果我們在這個回調中進行樣式的改動,涉及到的布局計算則會被判無效。如果在下一幀中有任何的讀取布局相關的操作,例如getBoundingClientRect,clientWidth等等,瀏覽器會不得不執行一次強制同步布局(Forced Synchronous Layout),這將是一個潛在的性能瓶頸。

  另一個不要在回調中觸發Dom改動的原因是,Dom改動是不可預期的,正因為如此,我們可以很容易地超過瀏覽器給出的時間限期。

  最佳的實踐就是只在requestAnimationFrame的回調中去進行dom的改動,因為瀏覽器會優化同類型的改動。這表明我們的代碼要在requestIdleCallback時使用文檔片段,這樣就能在下一個requestAnimationFrame回調中把所有改動的dom追加上去。如果你正在使用Virtual DOM這個庫,你可以使用requestIdleCallback進行Dom變動,但真正的Dom改動還是在下一個requestAnimationFrame的回調中,而不是requestIdleCallback的回調中。

  所以謹記上面說的,下面來看下代碼吧:

function processPendingElements (deadline) {

  // If there is no deadline, just run as long as necessary.
  if (typeof deadline === 'undefined')
    deadline = { timeRemaining: function () { return Number.MAX_VALUE } };

  if (!documentFragment)
    documentFragment = document.createDocumentFragment();

  // Go for as long as there is time remaining and work to do.
  while (deadline.timeRemaining() > 0 && elementsToAdd.length > 0) {

    // Create the element.
    var elToAdd = elementsToAdd.pop();
    var el = document.createElement(elToAdd.tag);
    el.textContent = elToAdd.content;

    // Add it to the fragment.
    documentFragment.appendChild(el);

    // Don't append to the document immediately, wait for the next
    // requestAnimationFrame callback.
    scheduleVisualUpdateIfNeeded();
  }

  // Check if there are more events still to send.
  if (elementsToAdd.length > 0)
    scheduleElementCreation();
}

 

  在上面,我創建了一個元素,而且使用添加上了textContent這個屬性。但這時候還不應該把元素追加到文檔流中去。創建完元素添加到文檔片段后,scheduleVisualUpdateIfNeeded則被調用,它會創建一個requestAnimationFrame的回調,這時候,我們就應該把文檔片段追加到body中去了:

function scheduleVisualUpdateIfNeeded() {

  if (isVisualUpdateScheduled)
    return;

  isVisualUpdateScheduled = true;

  requestAnimationFrame(appendDocumentFragment);
}

function appendDocumentFragment() {
  // Append the fragment and reset.
  document.body.appendChild(documentFragment);
  documentFragment = null;
}

  一切順利的話,我們則會看到追加dom到文檔中時,並沒有什么性能的損耗。真tm的棒!

 


免責聲明!

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



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