渲染性能分析(下)

上篇咱們大體分析了在處理JavaScript階段和Style階段須要注意的問題,這篇咱們就來看下在Layout、Paint、Composite階段以及處理用戶行爲的時候,應該關注的問題所在。css

避免大型的複雜的佈局和佈局限制

Layout階段瀏覽器將計算元素的大小,在頁面中的位置,其餘元素的影響等等,與樣式計算(Style calculation)相似,基本限制因素以下:git

  • 須要Layout的元素數量
  • Layout的複雜度

TL;DRgithub

  • Layout適用於整個文檔流
  • DOM的數量直接影響Layout的性能消耗,儘可能避免觸發Layout
  • 避免強制同步修改Layout,形成反覆Layout。即讀取style的值而後修改style

儘量的避免觸發Layout

當更改樣式時,瀏覽器會去檢查需不需從新計算觸發Layout,通常來講修改元素的幾何屬性(geometric properties)例如:寬高,佈局定位都會觸發Layout瀏覽器

.box {
  width: 200px;
  height: 200px;
}

// 改變元素寬高 觸發Layout
.box-expanded: {
  width: 300px;
  height: 300px;
}

Layout是做用於全局整個文檔流的,因此若是有大量的元素須要處理,就會消耗很長時間去計算這些元素的大小和定位。
若是沒法避免觸發Layout,能夠經過Performance查看Layout階段的耗時是不是影響性能的瓶頸。緩存

Snipaste_2019-10-23_17-24-29-4fe35133-c763-4d7a-90ac-84483eb9262c

在Performance中咱們能夠清楚的看到Layout階段消耗的時間,以及涉及的節點數(如圖爲314個元素)
https://csstriggers.com/ 列出了一些CSS屬性會觸發渲染的哪一個階段,能夠做爲對照參考。
另外使用flexbox佈局要比傳統的經過float或者相對定位絕對定位實現佈局更快。工具

避免出現強制同步佈局

正常狀況下渲染步驟是先執行JavaScript,而後是style calculation 而後觸發Layout。可是有種狀況是觸發Layout的時間點早於JavaScript的執行,這種狀況叫強制同步佈局(forced synchoronous layout)佈局

要明確的是在JavaScript運行時,前一幀的佈局屬性值都是已知的。舉個例子來講若是你想在幀(frame)開始前獲取某個元素的高度,就能夠這樣寫:性能

requestAnimtionFrame(logBoxHeight);

function logBoxHeight(){
  console.log(element.offsetHeight);
}

可是若是你先改變的元素的樣式而後在獲取元素高就會出問題flex

function logBoxHeight(){
  element.classList.add('big');
  console.log(element.offsetHeight);
}

如今的狀況就變成這樣,因爲添加了新的class後要輸入元素的offsetHeight,瀏覽器必須先從新進行佈局計算才能拿到正確的offsetHeight的值,這徹底是不必的,並且這個例子中一般狀況下都是不須要先去設置樣式再去取屬性值的,直接使用最後一幀的屬性值徹底足夠了。因此通常狀況下最好是先去讀取須要的屬性值,而後再作更改。優化

function logBoxHeight(){
  console.log(element.offsetHeight);
  element.classList.add('big');
}

還有一種更糟糕的狀況是反覆不斷的強制同步觸發layout。看下面的代碼

function resizeAllParagraphsToMatchBlockWidth(){
    // 讓瀏覽器陷入讀寫循環
  for(let i = 0; i < paragraphs.length; i++){
    paragraphs[i].style.width = element.offsetHeight + 'px';
  }
}

打眼一看好像沒什麼問題,其實這種問題很常見每次迭代都會去讀取element.offsetHeight屬性,而後用它去更新paragraph的width屬性。解決辦法也很常見就是讀取一次作一個緩存。

const width = element.offsetHeight;

function resizeAllparagraphsToMatchBlockWidth(){
    for(let i = 0; i < paragraphs.length;i++){
        paragraphs[i].style.width = width + 'px';
    }
}

簡化Paint複雜度,減小Paint的面積

Paint是一個填充像素(pixels)的過程,最終這些像素會經過合成器合成到屏幕。這個階段一般是渲染元素整個過程當中最消耗時間的階段,因此要儘量的避免

TL;DR

  • 除了transform和opacity屬性改變其餘任何屬性都會觸發Paint
  • 由於Paint在整個渲染過程當中是最消耗時間和性能的,因此儘量的避免觸發
  • 利用Chrome DevTools來觀察Paint階段,並儘量的下降減少對性能的消耗
  • 能夠經過提高圖層來減小Paint的面積大小

若是觸發Layout確定觸發Paint,由於改變元素的幾何屬性(寬高等)意味着須要從新佈局定位。固然修改一些非幾何屬性例如:background text-color,shadow這些也會觸發paint,只不過不會觸發layout因此整個渲染過程就會跳過Layout階段。
Snipaste_2019-10-24_19-17-20-a9b6b4bf-10d0-43b9-a72f-095c69237835

利用Chrome DevTools來觀察渲染過程當中最消耗性能的部分,能夠看到以下圖綠色部分表示的是須要被重繪的區域。
Snipaste_2019-10-24_19-26-52-e3d8d020-8732-4f82-a60f-779cdef8f440

可使用will-change屬性或者相似的hack手段讓瀏覽器建立一個新的圖層來減小須要被Paint的區域。關於will-change的詳細內容能夠看這篇文章【關於will-change屬性你須要知道的事】此處不在贅述。

儘量簡化Paint的過程,在Paint階段有的步驟是很是消耗性能的,好比任何涉及到模糊(blur)的過程(例如:shadow屬性),就CSS而言這些屬性之間看上去沒什麼性能上的差別,但實際在Paint階段是區別仍是很明顯的。

Composite

composite階段是將Paint過程當中的內容聚集起來顯示在屏幕上。

這個過程當中主要有兩個影響頁面性能的關鍵因素:一個是須要整合的合成層(compoaitor layers)數量,另外一個是用於動畫的相關屬性

TL;DR

  • 使用will-change或translateZ屬性作硬件加速
  • 避免建立過多圖層(layer),圖層會佔用內存
  • 對於動畫的操做使用transform和opacity作變動

渲染過程當中最好的狀況是避免觸發Layout和Paint只須要合成(compositing)階段處理變動。要作到這一點只須要一直使用只經過合成器處理的屬性便可。(只有transform和opacity屬性能夠作到)

Postion   transform: translate(npx,npx);
Scale     transform: scale(n);
Rotation  transform: rotate(ndeg);
Skew      transform: skew(X|Y)(ndeg);
Matrix    transform: matrix(3d)(...);
Opacity   opacity: 0 <= n <= 1

使用transform和opacity的注意點是對應元素要在自身的合成圖層,若是沒有自身圖層就要建立一個圖層。這裏涉及到建立圖層和硬件加速的內容能夠參考【關於will-change屬性你須要知道的事

經過提高或建立圖層有助於性能的提高這個技巧誘惑力是大的,因此有可能就會寫出以下代碼:

* {
    will-change: transform;
    transform: translateZ(0);
}

如同在 【關於will-change屬性你須要知道的事】裏提到的這種作法非但不能帶來性能上的提高,反而會佔用過多系統資源,對CPU和GPU都會帶來額外的負擔。

最後和咱們以前提到的相似Chrome DevTools提供了供開發者查看頁面圖層的工具,能夠看到當前頁面上有多少層級,每一個層級的大小、渲染的次數以及合成的緣由等等,咱們能夠經過這些信息去分析和作對應的優化。
Snipaste_2019-10-24_22-54-43-39b79651-7d26-493d-ae0a-74384cc8fbf2

對輸入處理程序作防抖

處理用戶輸入也是潛在的可能會影響性能的因素,由於其可能會阻塞其餘內容的加載而且致使沒必要要的佈局(layout)工做

TL;DR

  • 避免時間過長運行處理輸入程序,其會阻塞頁面滾動;
  • 不要在處理輸入的程序中修改樣式;
  • 對輸入處理程序作防抖,在下一幀的requestAnimationFrame回調中存儲事件值和樣式的更改

避免運行時間過長的處理程序

頁面交互最快的狀況是,當用戶與頁面交互時,頁面的合成器線程接受用戶的觸摸輸入並將內容四處移動。這個過程不須要與主線程通訊,而是直接提交給GPU處理。因此不須要等待主線程對JS的處理、以及佈局(layout)、繪製(paint)等操做完成。

Snipaste_2019-10-21_17-27-22-296a06f0-6a55-41a7-af01-ff773c8e1f7c

可是,若是附加了輸入處理程序(如touchstart,touchmove,或者touchend)後,合成器線程必須等待該處理程序執行完畢,由於有可能調用了preventDefault()來阻止觸摸滾動事件的發生。即便沒有調用preventDefault(),合成器也必須等待其執行完畢,這樣用戶的滾動操做就被阻止就可能致使幀丟失從而引發卡頓。
Snipaste_2019-10-21_17-28-10-7182cacd-7819-4f15-8957-594510110f12

總而言之,你應該確保運行的全部輸入處理程序都快速執行,並容許合成器執行其工做。

避免在輸入處理程序中改變樣式

輸入處理程序被安排在requestAnimtionFrame回調以前運行。若是在這個處理程序中作樣式上的修改,那麼在requestAnimationFrame開始處有須要更改的樣式處理,這會觸發強制同步佈局。

Snipaste_2019-10-21_17-41-01-f4a205b3-b672-4a2c-9ab0-e21e97d1d2d5

輸入處理程序作防抖

上面兩個問題的解決方案是相同的:你應該對下一個requestAnimationFrame的回調中作樣式更改的狀況作防抖處理。

function onScroll(evt){
    lastScrollY = window.scrollY;

    if(scheduleAnimationFrame)
        retun;
    scheduleAnimationFrame = true;
    requestAnimationFrame(readAndUpdatePage);
}

window.addEventListener('scroll',onScroll);

這樣作還有一個好處,就是保持輸入處理程序的輕量,由於這樣就不會阻塞好比滾動等其餘操做。

原文:渲染性能分析(下)

相關文章
相關標籤/搜索