許多網站和應用程式都有許多可執行的指令碼,您的 JavaScript 通常必須越快越好,但又想要避免系統在使用者瀏覽時看到 JavaScript。要是您在使用者捲動頁面時傳送數據分析資料,或是在使用者輕觸按鈕時將元素附加至 DOM,網頁應用程式可能會沒有回應,進而導致使用者體驗不佳。
幸好,現在有 API 能助您一臂之力:requestIdleCallback
。採用 requestAnimationFrame
之後,我們就能正確安排動畫播放時間,盡可能達到 60fps,就像在影格結束時有有空時間,或是使用者處於閒置狀態時,requestIdleCallback
也會排定工作時間。這表示,您隨時都能在不接觸使用者的情況下,完成工作。這項 Chrome 瀏覽器第 47 版起已推出,因此你可以立即使用 Chrome Canary 試試!它是一項實驗功能,其規格仍持續不斷,因此未來可能會有變化。
為什麼要使用 requestIdleCallback?
自行安排非必要工作並不容易。由於執行 requestAnimationFrame
回呼後,需要執行樣式計算、版面配置、繪製和其他瀏覽器內部,因此無法確切瞭解剩餘的影格時間。在家式解決方案無法涵蓋所有選擇。為確保使用者不會以某種方式互動,您還需要將事件監聽器附加至每種互動事件 (scroll
、touch
、click
),即使不需要使用這些事件來取得相關功能,只確保使用者不會進行互動。另一方面,瀏覽器可以在影格結束時明確知道有多少時間可供使用,以及使用者是否正在互動。因此,我們透過 requestIdleCallback
取得一個 API,讓我們能以最有效率的方式使用任何空閒時間。
以下將進一步說明,並看看可以如何運用。
正在檢查 requestIdleCallback
由於 requestIdleCallback
仍在早期,因此在您使用前,請務必先檢查是否可用於使用:
if ('requestIdleCallback' in window) {
// Use requestIdleCallback to schedule work.
} else {
// Do what you’d do today.
}
您也可以縮減其行為,而必須改回使用 setTimeout
:
window.requestIdleCallback =
window.requestIdleCallback ||
function (cb) {
var start = Date.now();
return setTimeout(function () {
cb({
didTimeout: false,
timeRemaining: function () {
return Math.max(0, 50 - (Date.now() - start));
}
});
}, 1);
}
window.cancelIdleCallback =
window.cancelIdleCallback ||
function (id) {
clearTimeout(id);
}
使用 setTimeout
不好,因為不像 requestIdleCallback
一樣知道閒置時間,但因為在 requestIdleCallback
無法使用的情況下,您會直接呼叫函式,因此這樣不會更糟。有了輔助程式,當 requestIdleCallback
可用時,您的通話將會自動重新導向,太棒了。
但現在,我們先假設這個網路確實存在。
使用 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
類似,但其差異在於它採用選用的第二個參數:具有逾時屬性的選項物件。此逾時設定 (如有設定) 會提供瀏覽器執行回呼所需的時間 (以毫秒為單位):
// Wait at most two seconds before processing events.
requestIdleCallback(processPendingAnalyticsEvents, { timeout: 2000 });
如果回呼是因為逾時觸發而執行,您會看到以下兩點:
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);
}
設定這項參數可能會造成使用者註意,因為此參數可能會導致應用程式沒有回應或卡頓。在可能的情況下,讓瀏覽器決定呼叫回呼的時機。
使用 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 秒的逾時時間,不過這個值必須視你的應用程式而定。就數據分析資料而言,如果希望系統能在合理的時間範圍內回報資料,而非只是將來的某個時間點回報,就適合使用逾時時間。
最後,我們必須編寫 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
不存在,則應立即傳送數據分析資料。不過在正式版應用程式中,建議您以逾時的方式延遲傳送,以確保不會與任何互動衝突並導致卡頓。
使用 requestIdleCallback 進行 DOM 變更
另一種情況是,當您必須進行非重要的 DOM 變更時 (例如,在不斷增加的延遲載入清單結尾加入項目),requestIdleCallback
確實有助於提升效能。讓我們來看看 requestIdleCallback
在一般頁框中實際的運作方式。
瀏覽器可能太繁忙而無法在特定影格中執行任何回呼,因此你應該不希望在影格結束時可用「任何」時間執行其他作業。這與 setImmediate
不同,後者「會」在每個影格執行。
如果在影格結束時觸發回呼,回呼會安排在修訂目前影格後執行,也就是套用樣式變更,重要的是計算版面配置。如果我們在閒置回呼內進行 DOM 變更,這些版面配置的計算就會失效。如果下一個頁框中有任何讀取版面配置的功能 (例如 getBoundingClientRect
、clientWidth
等),瀏覽器就必須執行強制同步版面配置,這可能會是效能瓶頸。
「閒置回呼」中無法觸發 DOM 變更的另一個原因是,變更 DOM 的時間是無法預測的,因此我們可以輕鬆超過瀏覽器提供的期限。
最佳做法是只在 requestAnimationFrame
回呼內進行 DOM 變更,因為瀏覽器會根據該類型的工作安排 DOM 的時程。也就是說,我們的程式碼需要使用文件片段,然後再附加至下一個 requestAnimationFrame
回呼。如果您使用 VDOM 程式庫,請使用 requestIdleCallback
進行變更,但您會在下一個 requestAnimationFrame
回呼中套用 DOM 修補程式,而不是閒置回呼。
瞭解這一點後,我們來看看程式碼:
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
回呼,接著將文件片段附加至主體:
function scheduleVisualUpdateIfNeeded() {
if (isVisualUpdateScheduled)
return;
isVisualUpdateScheduled = true;
requestAnimationFrame(appendDocumentFragment);
}
function appendDocumentFragment() {
// Append the fragment and reset.
document.body.appendChild(documentFragment);
documentFragment = null;
}
只要一切順利,當將項目附加至 DOM 時,就能減少資源浪費。棒極了!
常見問題
- 是否有 polyfill?
真可惜,但如果您想採用透明重新導向
setTimeout
的機制,那就很適合使用這種做法。這個 API 之所以存在,是因為其填補了網路平台的實際落差。要推斷缺乏活動並不容易,但目前沒有任何 JavaScript API 能判斷頁框末端的空時間,因此建議您最好進行猜測。setTimeout
、setInterval
或setImmediate
等 API 可用於排定工作,但並未及時避免使用者與requestIdleCallback
的互動。 - 如果超過期限,會怎麼樣?
如果
timeRemaining()
傳回 0,但您選擇執行更久,則可放心執行,不會擔心瀏覽器使工作停止。不過,瀏覽器會讓你有期限,確保使用者能享有流暢的體驗,因此,除非有充分理由,否則你依舊會遵循期限。 timeRemaining()
會傳回的最高值嗎?是,現在是 50 毫秒。嘗試維護一個回應應用程式時,所有對使用者互動的回應應維持在 100 毫秒以下。在多數情況下,使用者應與 50 毫秒視窗互動,然後允許閒置回呼執行完畢,並讓瀏覽器回應使用者互動。您可能會收到多個閒置的回呼排程 (如果瀏覽器判斷是否有足夠時間執行回呼)。- 我不應在 requestIdleCallback 中處理哪種工作?
在理想情況下,您的工作應該置於特徵相對可預測的小型區塊 (微任務)。例如,變更 DOM 會觸發樣式計算、版面配置、繪製和合成,因此會出現無法預測的執行時間。因此,建議您僅按照上述建議,在
requestAnimationFrame
回呼中變更 DOM。請特別留意的是解決 (或拒絕) Promise,因為當閒置回呼完成後,即使現在已經沒有剩餘時間,回呼將立即執行。 - 影格結束時我一律會收到
requestIdleCallback
嗎? 不行。每當影格結束時,或使用者處於閒置狀態時,瀏覽器就會安排回呼。您不應預期每個頁框都會呼叫回呼,而如果需要在指定時間範圍內執行回呼,則應使用逾時。 - 可以有多個
requestIdleCallback
回呼嗎?可以,而且非常有可能。您可以擁有多個requestAnimationFrame
回呼。但請記住,如果第一個回呼在回呼期間用完剩餘的時間,其他回呼就不再有剩餘時間。其他回呼則必須等到瀏覽器下次閒置後才能執行。視您要進行的工作而定,建議您設定一個閒置的回呼,並將工作劃分到其中。或者,您也可以使用逾時,確保沒有任何回呼都浪費時間。 - 如果在另一個內部設定新的閒置回呼,會發生什麼事? 新的閒置回呼將會排定盡快執行,從「下一個」影格 (而非目前的影格) 開始執行。
閒置!
requestIdleCallback
是確保執行程式碼的絕佳方式,但不會妨礙使用者操作。這項工具簡單易用,而且非常靈活。不過,由於仍處於初期階段,但規格並未充分調整,歡迎提出任何意見回饋。
快在 Chrome Canary 中一探究竟,試用專案功能,並告訴我們你的進度!