頁面流暢與 FPS
頁面是一幀一幀繪制出來的,當(dāng)每秒繪制的幀數(shù)(FPS)達(dá)到 60 時(shí),頁面是流暢的,小于這個值時(shí),用戶會感覺到卡頓。
1s 60幀,所以每一幀分到的時(shí)間是 1000/60 ≈ 16 ms。所以我們書寫代碼時(shí)力求不讓一幀的工作量超過 16ms。
Frame
那么瀏覽器每一幀都需要完成哪些工作?
通過上圖可看到,一幀內(nèi)需要完成如下六個步驟的任務(wù):
- 處理用戶的交互
- JS 解析執(zhí)行
- 幀開始。窗口尺寸變更,頁面滾去等的處理
- requestAnimationFrame(rAF)
- 布局
- 繪制
requestIdleCallback
上面六個步驟完成后沒超過 16 ms,說明時(shí)間有富余,此時(shí)就會執(zhí)行 requestIdleCallback
里注冊的任務(wù)。
從上圖也可看出,和 requestAnimationFrame
每一幀必定會執(zhí)行不同,requestIdleCallback
是撿瀏覽器空閑來執(zhí)行任務(wù)。
如此一來,假如瀏覽器一直處于非常忙碌的狀態(tài),requestIdleCallback
注冊的任務(wù)有可能永遠(yuǎn)不會執(zhí)行。此時(shí)可通過設(shè)置 timeout
(見下面 API 介紹)來保證執(zhí)行。
API
var handle = window.requestIdleCallback(callback[, options])
-
callback:回調(diào),即空閑時(shí)需要執(zhí)行的任務(wù),該回調(diào)函數(shù)接收一個
IdleDeadline
對象作為入?yún)ⅰF渲?code>IdleDeadline對象包含:-
didTimeout
,布爾值,表示任務(wù)是否超時(shí),結(jié)合timeRemaining
使用。 -
timeRemaining()
,表示當(dāng)前幀剩余的時(shí)間,也可理解為留給任務(wù)的時(shí)間還有多少。
-
-
options:目前 options 只有一個參數(shù)
-
timeout
。表示超過這個時(shí)間后,如果任務(wù)還沒執(zhí)行,則強(qiáng)制執(zhí)行,不必等待空閑。
-
IdleDeadline
對象參考MDN:https://developer.mozilla.org/zh-CN/docs/Web/API/IdleDeadline
示例
requestIdleCallback(myNonEssentialWork, { timeout: 2000 });
?
// 任務(wù)隊(duì)列
const tasks = [
() => {
console.log("第一個任務(wù)");
},
() => {
console.log("第二個任務(wù)");
},
() => {
console.log("第三個任務(wù)");
},
];
?
function myNonEssentialWork (deadline) {
// 如果幀內(nèi)有富余的時(shí)間,或者超時(shí)
while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && tasks.length > 0) {
work();
}
?
if (tasks.length > 0)
requestIdleCallback(myNonEssentialWork);
}
?
function work () {
tasks.shift()();
console.log('執(zhí)行任務(wù)');
}
超時(shí)的情況,其實(shí)就是瀏覽器很忙,沒有空閑時(shí)間,此時(shí)會等待指定的 timeout
那么久再執(zhí)行,通過入?yún)?dealine
拿到的 didTmieout
會為 true
,同時(shí) timeRemaining ()
返回的也是 0。超時(shí)的情況下如果選擇繼續(xù)執(zhí)行的話,肯定會出現(xiàn)卡頓的,因?yàn)楸厝粫⒁粠臅r(shí)間拉長。
cancelIdleCallback
與 setTimeout
類似,返回一個唯一 id,可通過 cancelIdleCallback
來取消任務(wù)。
總結(jié)
一些低優(yōu)先級的任務(wù)可使用 requestIdleCallback
等瀏覽器不忙的時(shí)候來執(zhí)行,同時(shí)因?yàn)闀r(shí)間有限,它所執(zhí)行的任務(wù)應(yīng)該盡量是能夠量化,細(xì)分的微任務(wù)(micro task)。
因?yàn)樗l(fā)生在一幀的最后,此時(shí)頁面布局已經(jīng)完成,所以不建議在 requestIdleCallback
里再操作 DOM,這樣會導(dǎo)致頁面再次重繪。DOM 操作建議在 rAF 中進(jìn)行。同時(shí),操作 DOM 所需要的耗時(shí)是不確定的,因?yàn)闀?dǎo)致重新計(jì)算布局和視圖的繪制,所以這類操作不具備可預(yù)測性。
Promise 也不建議在這里面進(jìn)行,因?yàn)?Promise 的回調(diào)屬性 Event loop 中優(yōu)先級較高的一種微任務(wù),會在 requestIdleCallback
結(jié)束時(shí)立即執(zhí)行,不管此時(shí)是否還有富余的時(shí)間,這樣有很大可能會讓一幀超過 16 ms。
額外補(bǔ)充一下window.requestAnimationFrame
在沒有 requestAnimationFrame
方法的時(shí)候,執(zhí)行動畫,我們可能使用 setTimeout
或 setInterval
來觸發(fā)視覺變化;但是這種做法的問題是:回調(diào)函數(shù)執(zhí)行的時(shí)間是不固定的,可能剛好就在末尾,或者直接就不執(zhí)行了,經(jīng)常會引起丟幀而導(dǎo)致頁面卡頓。
歸根到底發(fā)生上面這個問題的原因在于時(shí)機(jī),也就是瀏覽器要知道何時(shí)對回調(diào)函數(shù)進(jìn)行響應(yīng)。setTimeout
或 setInterval
是使用定時(shí)器來觸發(fā)回調(diào)函數(shù)的,而定時(shí)器并無法保證能夠準(zhǔn)確無誤的執(zhí)行,有許多因素會影響它的運(yùn)行時(shí)機(jī),比如說:當(dāng)有同步代碼執(zhí)行時(shí),會先等同步代碼執(zhí)行完畢,異步隊(duì)列中沒有其他任務(wù),才會輪到自己執(zhí)行。并且,我們知道每一次重新渲染的最佳時(shí)間大約是 16.6 ms,如果定時(shí)器的時(shí)間間隔過短,就會造成 過度渲染,增加開銷;過長又會延遲渲染,使動畫不流暢。
requestAnimationFrame
方法不同與 setTimeout
或 setInterval
,它是由系統(tǒng)來決定回調(diào)函數(shù)的執(zhí)行時(shí)機(jī)的,會請求瀏覽器在下一次重新渲染之前執(zhí)行回調(diào)函數(shù)。無論設(shè)備的刷新率是多少,requestAnimationFrame
的時(shí)間間隔都會緊跟屏幕刷新一次所需要的時(shí)間;例如某一設(shè)備的刷新率是 75 Hz,那這時(shí)的時(shí)間間隔就是 13.3 ms(1 秒 / 75 次)。需要注意的是這個方法雖然能夠保證回調(diào)函數(shù)在每一幀內(nèi)只渲染一次,但是如果這一幀有太多任務(wù)執(zhí)行,還是會造成卡頓的;因此它只能保證重新渲染的時(shí)間間隔最短是屏幕的刷新時(shí)間。
requestAnimationFrame
方法的具體說明可以看 MDN 的相關(guān)文檔,下面通過一個網(wǎng)頁動畫的示例來了解一下如何使用。
let offsetTop = 0;
const div = document.querySelector(".div");
const run = () => {
div.style.transform = `translate3d(0, ${offsetTop += 10}px, 0)`;
window.requestAnimationFrame(run);
};
run();
如果想要實(shí)現(xiàn)動畫效果,每一次執(zhí)行回調(diào)函數(shù),必須要再次調(diào)用 requestAnimationFrame
方法;與 setTimeout
實(shí)現(xiàn)動畫效果的方式是一樣的,只不過不需要設(shè)置時(shí)間間隔。