網(wǎng)頁性能管理詳解:淺談chrome-Timeline及window.requestAnimationFrame()方法

標(biāo)簽:

你遇到過性能很差的網(wǎng)頁嗎?

這種網(wǎng)頁響應(yīng)非常緩慢,占用大量的CPU和內(nèi)存,瀏覽起來常常有卡頓,頁面的動畫效果也不流暢。

你會有什么反應(yīng)?我猜想,大多數(shù)用戶會關(guān)閉這個頁面,改為訪問其他網(wǎng)站。作為一個開發(fā)者,肯定不愿意看到這種情況,那么怎樣才能提高性能呢?

本文將詳細(xì)介紹性能問題的出現(xiàn)原因,以及解決方法。


一、網(wǎng)頁生成的過程


網(wǎng)頁的生成過程,大致可以分成五步。

HTML代碼轉(zhuǎn)化成DOM

CSS代碼轉(zhuǎn)化成CSSOM(CSS Object Model)

結(jié)合DOM和CSSOM,生成一棵渲染樹(包含每個節(jié)點的視覺信息)

生成布局(layout),即將所有渲染樹的所有節(jié)點進行平面合成

將布局繪制(paint)在屏幕上

這五步里面,第一步到第三步都非???,耗時的是第四步和第五步。

"生成布局"(flow)和"繪制"(paint)這兩步,合稱為"渲染"(render)。


二、重排和重繪

網(wǎng)頁生成的時候,至少會渲染一次。用戶訪問的過程中,還會不斷重新渲染。

以下三種情況,會導(dǎo)致網(wǎng)頁重新渲染。

修改DOM

修改樣式表

用戶事件(比如鼠標(biāo)懸停、頁面滾動、輸入框鍵入文字、改變窗口大小等等)

重新渲染,就需要重新生成布局和重新繪制。前者叫做"重排"(reflow),后者叫做"重繪"(repaint)。

需要注意的是,"重繪"不一定需要"重排",比如改變某個網(wǎng)頁元素的顏色,就只會觸發(fā)"重繪",不會觸發(fā)"重排",因為布局沒有改變。但是,"重排"必然導(dǎo)致"重繪",比如改變一個網(wǎng)頁元素的位置,就會同時觸發(fā)"重排"和"重繪",因為布局改變了。

三、對于性能的影響

重排和重繪會不斷觸發(fā),這是不可避免的。但是,它們非常耗費資源,是導(dǎo)致網(wǎng)頁性能低下的根本原因。

提高網(wǎng)頁性能,就是要降低"重排"和"重繪"的頻率和成本,盡量少觸發(fā)重新渲染。

前面提到,DOM變動和樣式變動,都會觸發(fā)重新渲染。但是,瀏覽器已經(jīng)很智能了,會盡量把所有的變動集中在一起,排成一個隊列,然后一次性執(zhí)行,盡量避免多次重新渲染。

div.style.color=‘blue‘;div.style.marginTop=‘30px‘;

上面代碼中,div元素有兩個樣式變動,但是瀏覽器只會觸發(fā)一次重排和重繪。

如果寫得不好,就會觸發(fā)兩次重排和重繪。

div.style.color=‘blue‘;

var margin=parseInt(div.style.marginTop);

div.style.marginTop=(margin+10)+‘px‘;

上面代碼對div元素設(shè)置背景色以后,第二行要求瀏覽器給出該元素的位置,所以瀏覽器不得不立即重排。

一般來說,樣式的寫操作之后,如果有下面這些屬性的讀操作,都會引發(fā)瀏覽器立即重新渲染。

offsetTop/offsetLeft/offsetWidth/offsetHeight

scrollTop/scrollLeft/scrollWidth/scrollHeight

clientTop/clientLeft/clientWidth/clientHeight

getComputedStyle()

所以,從性能角度考慮,盡量不要把讀操作和寫操作,放在一個語句里面。

// bad

div.style.left= div.offsetLeft+10+"px";

div.style.top= div.offsetTop+10+"px";

// good

var left= div.offsetLeft;

var top= div.offsetTop;

div.style.left= left+10+"px";

div.style.top= top+10+"px";

一般的規(guī)則是:

樣式表越簡單,重排和重繪就越快。

重排和重繪的DOM元素層級越高,成本就越高。

table元素的重排和重繪成本,要高于div元素

四、提高性能的九個技巧

有一些技巧,可以降低瀏覽器重新渲染的頻率和成本。

第一條是上一節(jié)說到的,DOM 的多個讀操作(或多個寫操作),應(yīng)該放在一起。不要兩個讀操作之間,加入一個寫操作。

第二條,如果某個樣式是通過重排得到的,那么最好緩存結(jié)果。避免下一次用到的時候,瀏覽器又要重排。

第三條,不要一條條地改變樣式,而要通過改變class,或者csstext屬性,一次性地改變樣式。

// bad

var left=10;

var top=10;

el.style.left= left+"px";

el.style.top= top+"px";

// good?

el.className+=" theclassname";

// good

el.style.cssText+=";?

left: "+ left+"px;

top: "+ top+"px;";

第四條,盡量使用離線DOM,而不是真實的網(wǎng)面DOM,來改變元素樣式。比如,操作Document Fragment對象,完成后再把這個對象加入DOM。再比如,使用 cloneNode() 方法,在克隆的節(jié)點上進行操作,然后再用克隆的節(jié)點替換原始節(jié)點。

第五條,先將元素設(shè)為 display: none (需要1次重排和重繪),然后對這個節(jié)點進行100次操作,最后再恢復(fù)顯示(需要1次重排和重繪)。這樣一來,你就用兩次重新渲染,取代了可能高達(dá)100次的重新渲染。

第六條,position屬性為absolute或fixed的元素,重排的開銷會比較小,因為不用考慮它對其他元素的影響。

第七條,只在必要的時候,才將元素的display屬性為可見,因為不可見的元素不影響重排和重繪。另外,visibility : hidden 的元素只對重排有影響,不影響重繪。

第八條,使用虛擬DOM的腳本庫,比如React等。

第九條,使用 window.requestAnimationFrame()、window.requestIdleCallback() 這兩個方法調(diào)節(jié)重新渲染(詳見后文)。

五、刷新率

很多時候,密集的重新渲染是無法避免的,比如scroll事件的回調(diào)函數(shù)和網(wǎng)頁動畫。

網(wǎng)頁動畫的每一幀(frame)都是一次重新渲染。每秒低于24幀的動畫,人眼就能感受到停頓。一般的網(wǎng)頁動畫,需要達(dá)到每秒30幀到60幀的頻率,才能比較流暢。如果能達(dá)到每秒70幀甚至80幀,就會極其流暢。


大多數(shù)顯示器的刷新頻率是60Hz,為了與系統(tǒng)一致,以及節(jié)省電力,瀏覽器會自動按照這個頻率,刷新動畫(如果可以做到的話)。


所以,如果網(wǎng)頁動畫能夠做到每秒60幀,就會跟顯示器同步刷新,達(dá)到最佳的視覺效果。這意味著,一秒之內(nèi)進行60次重新渲染,每次重新渲染的時間不能超過16.66毫秒。


一秒之間能夠完成多少次重新渲染,這個指標(biāo)就被稱為"刷新率",英文為FPS(frame per second)。60次重新渲染,就是60FPS。

六、開發(fā)者工具的Timeline面板

Chrome瀏覽器開發(fā)者工具的Timeline面板,是查看"刷新率"的最佳工具。這一節(jié)介紹如何使用這個工具。

首先,按下 F12 打開"開發(fā)者工具",切換到Timeline面板。


左上角有一個灰色的圓點,這是錄制按鈕,按下它會變成紅色。然后,在網(wǎng)頁上進行一些操作,再按一次按鈕完成錄制。

Timeline面板提供兩種查看方式:橫條的是"事件模式"(Event Mode),顯示重新渲染的各種事件所耗費的時間;豎條的是"幀模式"(Frame Mode),顯示每一幀的時間耗費在哪里。

先看"事件模式",你可以從中判斷,性能問題發(fā)生在哪個環(huán)節(jié),是JavaScript的執(zhí)行,還是渲染?


不同的顏色表示不同的事件。


藍(lán)色:網(wǎng)絡(luò)通信和HTML解析

黃色:JavaScript執(zhí)行

紫色:樣式計算和布局,即重排

綠色:重繪

哪種色塊比較多,就說明性能耗費在那里。色塊越長,問題越大。


幀模式(Frames mode)用來查看單個幀的耗時情況。每幀的色柱高度越低越好,表示耗時少。


你可以看到,幀模式有兩條水平的參考線。

下面的一條是60FPS,低于這條線,可以達(dá)到每秒60幀;上面的一條是30FPS,低于這條線,可以達(dá)到每秒30次渲染。如果色柱都超過30FPS,這個網(wǎng)頁就有性能問題了。

此外,還可以查看某個區(qū)間的耗時情況。


或者點擊每一幀,查看該幀的時間構(gòu)成。


七、window.requestAnimationFrame()

有一些JavaScript方法可以調(diào)節(jié)重新渲染,大幅提高網(wǎng)頁性能。

其中最重要的,就是 window.requestAnimationFrame() 方法。它可以將某些代碼放到下一次重新渲染時執(zhí)行。

function doubleHeight(element){

? ? ? var currentHeight= element.clientHeight;??

? ? ? element.style.height=(currentHeight*2)+‘px‘;

}

elements.forEach(doubleHeight);

上面的代碼使用循環(huán)操作,將每個元素的高度都增加一倍??墒?,每次循環(huán)都是,讀操作后面跟著一個寫操作。這會在短時間內(nèi)觸發(fā)大量的重新渲染,顯然對于網(wǎng)頁性能很不利。

我們可以使用 window.requestAnimationFrame(),讓讀操作和寫操作分離,把所有的寫操作放到下一次重新渲染。

function doubleHeight(element){

? ?var currentHeight= element.clientHeight;??

?window.requestAnimationFrame(function(){? ??

? ?element.style.height=(currentHeight*2)+‘px‘;

});

}

elements.forEach(doubleHeight);

頁面滾動事件(scroll)的監(jiān)聽函數(shù),就很適合用 window.requestAnimationFrame() ,推遲到下一次重新渲染。

$(window).on(‘scroll‘,function(){?

? window.requestAnimationFrame(scrollHandler);

});

當(dāng)然,最適用的場合還是網(wǎng)頁動畫。下面是一個旋轉(zhuǎn)動畫的例子,元素每一幀旋轉(zhuǎn)1度。

var rAF = window.requestAnimationFrame;

var degrees = 0;

function update() {

div.style.transform = "rotate(" + degrees + "deg)";

console.log(‘updated to degrees ‘ + degrees);

degrees = degrees + 1;

rAF(update);

}

rAF(update);

八、window.requestIdleCallback()

還有一個函數(shù)window.requestIdleCallback(),也可以用來調(diào)節(jié)重新渲染。

它指定只有當(dāng)一幀的末尾有空閑時間,才會執(zhí)行回調(diào)函數(shù)。

requestIdleCallback(fn);

上面代碼中,只有當(dāng)前幀的運行時間小于16.66ms時,函數(shù)fn才會執(zhí)行。否則,就推遲到下一幀,如果下一幀也沒有空閑時間,就推遲到下下一幀,以此類推。

它還可以接受第二個參數(shù),表示指定的毫秒數(shù)。如果在指定 的這段時間之內(nèi),每一幀都沒有空閑時間,那么函數(shù)fn將會強制執(zhí)行。

requestIdleCallback(fn,5000);

上面的代碼表示,函數(shù)fn最遲會在5000毫秒之后執(zhí)行。

函數(shù) fn 可以接受一個 deadline 對象作為參數(shù)。

requestIdleCallback(function someHeavyComputation(deadline) {

while(deadline.timeRemaining() > 0) {

doWorkIfNeeded();

}

if(thereIsMoreWorkToDo) {

requestIdleCallback(someHeavyComputation);

}

});

上面代碼中,回調(diào)函數(shù) someHeavyComputation 的參數(shù)是一個 deadline 對象。

deadline對象有一個方法和一個屬性:timeRemaining() 和 didTimeout。

(1)timeRemaining() 方法

timeRemaining() 方法返回當(dāng)前幀還剩余的毫秒。這個方法只能讀,不能寫,而且會動態(tài)更新。因此可以不斷檢查這個屬性,如果還有剩余時間的話,就不斷執(zhí)行某些任務(wù)。一旦這個屬性等于0,就把任務(wù)分配到下一輪requestIdleCallback。

前面的示例代碼之中,只要當(dāng)前幀還有空閑時間,就不斷調(diào)用doWorkIfNeeded方法。一旦沒有空閑時間,但是任務(wù)還沒有全執(zhí)行,就分配到下一輪requestIdleCallback。

(2)didTimeout屬性

deadline對象的didTimeout屬性會返回一個布爾值,表示指定的時間是否過期。這意味著,如果回調(diào)函數(shù)由于指定時間過期而觸發(fā),那么你會得到兩個結(jié)果。

timeRemaining方法返回0

didTimeout 屬性等于 true

因此,如果回調(diào)函數(shù)執(zhí)行了,無非是兩種原因:當(dāng)前幀有空閑時間,或者指定時間到了。

function myNonEssentialWork (deadline) {

while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && tasks.length > 0)

doWorkIfNeeded();

if (tasks.length > 0)

requestIdleCallback(myNonEssentialWork);

}

requestIdleCallback(myNonEssentialWork, 5000);

上面代碼確保了,doWorkIfNeeded 函數(shù)一定會在將來某個比較空閑的時間(或者在指定時間過期后)得到反復(fù)執(zhí)行。

requestIdleCallback 是一個很新的函數(shù),剛剛引入標(biāo)準(zhǔn),目前只有Chrome支持。


九、參考鏈接

How browsers work

Domenico De Felice,

Rendering: repaint, reflow/relayout, restyle

Stoyan Stefanov,

Improving Web App Performance With the Chrome DevTools Timeline and Profiles

Addy Osmani,

Jank Busting for Better Rendering Performance

Tom Wiltzius,

Using requestIdleCallback

Paul Lewis,

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 問答題47 /72 常見瀏覽器兼容性問題與解決方案? 參考答案 (1)瀏覽器兼容問題一:不同瀏覽器的標(biāo)簽?zāi)J(rèn)的外補...
    _Yfling閱讀 14,128評論 1 92
  • 一:在制作一個Web應(yīng)用或Web站點的過程中,你是如何考慮他的UI、安全性、高性能、SEO、可維護性以及技術(shù)因素的...
    Arno_z閱讀 1,361評論 0 1
  • 網(wǎng)頁生成的過程 要理解網(wǎng)頁性能為什么不好,就要了解網(wǎng)頁是怎么生成的。 網(wǎng)頁的生成過程,大致可以分成五步:1.HTM...
    葶寳寳閱讀 1,308評論 0 8
  • 導(dǎo)語: 文中多處引用牛人博客觀點,重在分享知識和自己的一些見解~ 一,瀏覽器渲染頁面流程 1.瀏覽器解析html源...
    LORI地盤閱讀 385評論 0 0
  • 愛上獨處 最近越來越喜歡自己待著,忘了撒嬌,不愿在他面前露出幼稚的一面,發(fā)現(xiàn)自己愛上獨處。漸漸心灰意冷,不愿和同在...
    柚子茶香閱讀 404評論 1 3

友情鏈接更多精彩內(nèi)容