css 渲染優化

散一群逗逼發表於2022-06-01
在日常的開發中,通過使用css屬性,做一些動效、動畫時,會發現在頁面有卡頓;
在Android低端機尤為明顯,故需要知道瀏覽器渲染以及優化手段

瀏覽器渲染流程

image.png
image.png

  • 構建 DOM 樹:瀏覽器將 HTML 解析成樹形結構的 DOM 樹,一般來說,這個過程發生在頁面初次載入,或頁面 JavaScript 修改了節點結構的時候。
  • 構建渲染樹:瀏覽器將 CSS 解析成樹形結構的 CSSOM 樹,再和 DOM 樹合併成渲染樹。
  • 佈局(Layout):瀏覽器根據渲染樹所體現的節點、各個節點的CSS定義以及它們的從屬關係,計算出每個節點在螢幕中的位置。Web 頁面中元素的佈局是相對的,在頁面元素位置、大小發生變化,往往會導致其他節點聯動,需要重新計算佈局,這時候的佈局過程一般被稱為迴流/重排(Reflow)。
  • 繪製(Paint):遍歷渲染樹,呼叫渲染器的 paint() 方法在螢幕上繪製出節點內容,本質上是一個畫素填充的過程。這個過程也出現於迴流或一些不影響佈局的 CSS 修改引起的螢幕區域性重畫,這時候它被稱為重繪(Repaint)。實際上,繪製過程是在多個層上完成的,這些層我們稱為渲染層(RenderLayer)
  • 渲染層合成(Composite):多個繪製後的渲染層按照恰當的重疊順序進行合併,而後生成點陣圖,最終通過顯示卡展示到螢幕上。

以上是瀏覽器渲染的基本步驟,簡述:

DOM tree + CSS tree  == Render tree  ==>   Layout tree  ==>  PaintLayer => Composite
                                           PaintLayer => Composite
                                            

只要頁面上元素有改變,都會重複渲染以上部分步驟,在一幀內完成(16.77ms);
通過設定css 樣式 ,做動畫,在一幀內無法渲染完,甚至來不及渲染,就會卡頓;
其中部分渲染卡頓,跟渲染層合成(Composite)有關係。

Composite

什麼是渲染層合成

在 DOM 樹中每個節點都會對應一個渲染物件(RenderObject),當它們的渲染物件處於相同的座標空間(z 軸空間)時,就會形成一個 RenderLayers,也就是渲染層。渲染層將保證頁面元素以正確的順序堆疊,這時候就會出現層合成(composite),從而正確處理透明元素和重疊元素的顯示,這步叫做“層疊上下文”
這個模型類似於 Photoshop 的圖層模型,在 Photoshop 中,每個設計元素都是一個獨立的圖層,多個圖層以恰當的順序在 z 軸空間上疊加,最終構成一個完整的設計圖。
對於有位置重疊的元素的頁面,這個過程尤其重要,因為一旦圖層的合併順序出錯,將會導致元素顯示異常。

image.png

渲染物件(RenderObject)

一個 DOM 節點對應了一個渲染物件,渲染物件依然維持著 DOM 樹的樹形結構。

渲染層(RenderLayer)

這是瀏覽器渲染期間構建的第一個層模型,處於相同座標空間(z軸空間)的渲染物件,都將歸併到同一個渲染層中,因此根據層疊上下文,不同坐標空間的的渲染物件將形成多個渲染層,以體現它們的層疊關係。
因此滿足形成層疊上下文條件的 RenderObject 一定會為其建立新的渲染層,當然還有其他的一些特殊情況,為一些特殊的 RenderObject 建立一個新的渲染層,比如 overflow != visible 的元素。根據建立 RenderLayer 的原因不同,可以將其分為常見的 3 類:

  • NormalPaintLayer
  • 根元素(HTML)
  • 有明確的定位屬性(relative、fixed、sticky、absolute)
  • 透明的(opacity 小於 1)
  • 有 CSS 濾鏡(fliter)
  • 有 CSS mask 屬性
  • 有 CSS mix-blend-mode 屬性(不為 normal)
  • 有 CSS transform 屬性(不為 none)
  • backface-visibility 屬性為 hidden
  • 有 CSS reflection 屬性
  • 有 CSS column-count 屬性(不為 auto)或者 有 CSS column-width 屬性(不為 auto)
  • 當前有對於 opacity、transform、fliter、backdrop-filter 應用動畫
  • OverflowClipPaintLayer
  • overflow 不為 visible
  • NoPaintLayer
  • 不需要 paint 的 PaintLayer,比如一個沒有視覺屬性(背景、顏色、陰影等)的空 div。

滿足以上條件的 RenderObject 會擁有獨立的渲染層,而其他的 RenderObject 則和其第一個擁有渲染層的父元素共用一個。

圖形層(GraphicsLayer)

渲染層,和其第一個擁有 GraphicsLayer 的父層共用一個。
每個 GraphicsLayer 都有一個 GraphicsContext,GraphicsContext 會負責輸出該層的點陣圖,點陣圖是儲存在共享記憶體中,作為紋理上傳到 GPU 中,最後由 GPU 將多個點陣圖進行合成,然後 draw 到螢幕上,此時,我們的頁面也就展現到了螢幕上。

合成層(CompositingLayer)

滿足某些特殊條件的渲染層,會被瀏覽器自動提升為合成層。合成層擁有單獨的 GraphicsLayer,
個渲染層滿足哪些特殊條件時,才能被提升為合成層呢?這裡列舉了一些常見的情況:

  • 3D transforms:translate3d、translateZ 等
  • video、canvas、iframe 等元素
  • 通過 Element.animate() 實現的 opacity 動畫轉換
  • 通過 СSS 動畫實現的 opacity 動畫轉換
  • position: fixed
  • 具有 will-change 屬性
  • 對 opacity、transform、fliter、backdropfilter 應用了 animation 或者 transition

常見實用**transforms:translate3d、translateZ ** 來形成合成層,這樣是對原先的佈局沒有影響

示例

image.png
image.png

其他合成情況

隱式合成

除此之外,在瀏覽器的 Composite 階段,還存在一種隱式合成,部分渲染層在一些特定場景下,會被預設提升為合成層。其實就是元素div之間相互重疊
image.png
上圖所示:把position-1生成合成層,後面的元素會相互重疊,為了層疊上下文展示正確;導致後面的元素直接隱式合成了;

合層壓縮

如上圖,目前沒有遇到

合成爆炸

通過上圖研究,很容易就產生一些不在預期範圍內的合成層,當這些不符合預期的合成層達到一定量級時,就會變成層爆炸。
需要排查是否有重疊,能否通過z-index來調節層疊順序

優化

優點

  • 合成層的點陣圖,會交由 GPU 合成,比 CPU 處理要快得多;
  • 當需要 repaint 時,只需要 repaint 本身,不會影響到其他的層;
  • 元素提升為合成層後,transform 和 opacity 才不會觸發 repaint,如果不是合成層,則其依然會觸發 repaint。(只有css動畫,才可以,js去控制還是會重排、重繪的)使用 transform 或者 opacity 來實現動畫效果

更新元素幾何屬性(發生重排)
image.png
瀏覽器會觸發重新佈局,解析之後的一系列子階段,這個過程就叫重排。無疑,重排需要更新完整的渲染流水線,所以開銷也是最大的
引發重排的情況

  • 頁面首次渲染;
  • 瀏覽器視窗大小發生變化;
  • 元素的內容發生變化;
  • 元素的尺寸或者位置發生變化;
  • 元素的字型大小發生變化;
  • 啟用CSS偽類;
  • 查詢某些屬性或者呼叫某些方法;

    • offsetTop、offsetLeft、offsetWidth、offsetHeight
    • scrollTop、scrollLeft、scrollWidth、scrollHeight
    • clientTop、clientLeft、clientWidth、clientHeight
    • width、height
    • getComputedStyle()
    • getBoundingClientRect()
    • 新增或者刪除可見的DOM元素。

更新元素繪製屬性(發生重繪)
image.png
如果修改了元素的背景顏色,那麼佈局階段將不會被執行,因為並沒有引起幾何位置的變換,所以就直接進入了繪製階段,然後執行之後的一系列子階段,這個過程就叫重繪。相較於重排操作,重繪省去了佈局和分層階段,所以執行效率會比重排操作要高一些
引發重繪的情況

  • 重排
  • visibility: visible <=> hidden
  • 顏色改變
  • 其他幾何變化...

直接合成階段
image.png
我們使用了CSS的transform來實現動畫效果,這可以避開重排和重繪階段,直接在非主執行緒上執行合成動畫操作。這樣的效率是最高的,因為是在非主執行緒上合成,並沒有佔用主執行緒的資源,另外也避開了佈局和繪製兩個子階段,所以相對於重繪和重排,合成能大大提升繪製效率。
缺點

  • 繪製的圖層必須傳輸到 GPU,這些層的數量和大小達到一定量級後,可能會導致傳輸非常慢,進而導致一些低端和中端裝置上出現閃爍;
  • 隱式合成容易產生過量的合成層,每個合成層都佔用額外的記憶體,而記憶體是移動裝置上的寶貴資源,過多使用記憶體可能會導致瀏覽器崩潰,讓效能優化適得其s

    其他優化

    css

  • 使用 transform 替代 top
  • 使用 visibility 替換 display: none ,因為前者只會引起重繪,後者會引發迴流(改變了佈局
  • 避免使用table佈局,可能很小的一個小改動會造成整個 table 的重新佈局。
  • 儘可能在DOM樹的最末端改變class,迴流是不可避免的,但可以減少其影響。儘可能在DOM樹的最末端改變class,可以限制了迴流的範圍,使其影響儘可能少的節點。
  • 避免設定多層內聯樣式,CSS 選擇符從右往左匹配查詢,避免節點層級過多。
  • 將動畫效果應用到position屬性為absolute或fixed的元素上,避免影響其他元素的佈局,這樣只是一個重繪,而不是迴流,同時,控制動畫速度可以選擇 requestAnimationFrame,詳見探討 requestAnimationFrame
  • 避免使用CSS表示式,可能會引發迴流。
  • 將頻繁重繪或者回流的節點設定為圖層,圖層能夠阻止該節點的渲染行為影響別的節點,例如will-change、video、iframe等標籤,瀏覽器會自動將該節點變為圖層。

    js:

  • 避免頻繁操作樣式,最好一次性重寫style屬性,或者將樣式列表定義為class並一次性更改class屬性。
  • 避免頻繁操作DOM,建立一個documentFragment,在它上面應用所有DOM操作,最後再把它新增到文件中。
  • 避免頻繁讀取會引發迴流/重繪的屬性,如果確實需要多次使用,就用一個變數快取起來。
  • 對具有複雜動畫的元素使用絕對定位,使它脫離文件流,否則會引起父元素及後續元素頻繁迴流。

    總結

    在做動畫時儘可能的使用使用csstransforms屬性做動畫,,它跳過Layout和Paint階段,直接進入渲染,所以會提升部分效能,同時看看所屬空間位置,是否提升了合成層;還有注意元素是否重疊,防止合成層爆炸;這樣至少能保證css 方面,儘可能效能沒有問題。
    剩下的請檢視其他優化手段

示例程式碼

重疊

<style>
 .ball-running,.ball-running1 {
      width: 100px;
      height: 100px;
      animation: run-around 4s linear alternate 100;
      background: red;
      position: absolute;
      border-radius: 50%;
    }
    @keyframes run-around {
      0% {
        top: 0;
        left: 0;
      }
      25% {
        top: 0;
        left: 200px;
      }
      50% {
        top: 200px;
        left: 200px;
      }
      75% {
        top: 200px;
        left: 0;
      }
    }
    .position-1,.position-2,.position-3,.position-4 {
      position: absolute;
      width: 100px;
      height: 100px;
      background-color: red;
      border: #1b1b1b 1px solid;
    }
    .position-1 {
      transform: translateZ(10px);
    }
    .position-2 {
      top: 90px;
      left: 90px;
     // transform: translateZ(10px);
    }
    .position-3 {
      top: 180px;
      left: 180px;
    }
    .position-4 {
      top: 270px;
      left: 270px;
    }
    .parent-1 {
      //width: 100px;
      //height: 200px;
      background: #5b3a13;
      //overflow: auto;
      //transform: translateZ(10px);
    }
    .position-5 {

    }
<style/>    
<div class="parent-1">
      <div class="position-1">
        1
      </div>
      <div class="position-2">
        2
      </div>
      <div class="position-3">
        3
      </div>
      <div class="position-4">
        4
      </div>
    </div>

動畫

<style>
.ball-running,.ball-running1 {
      width: 100px;
      height: 100px;
    //  animation: run-around2 4s linear alternate 100;
      background: red;
      position: absolute;
      border-radius: 50%;
    }
    @keyframes run-around {
      0% {
        top: 0;
        left: 0;
      }
      25% {
        top: 0;
        left: 200px;
      }
      50% {
        top: 200px;
        left: 200px;
      }
      75% {
        top: 200px;
        left: 0;
      }
    }





    //.ball-running {
    //  width: 100px;
    //  height: 100px;
    //  animation: run-around 2s linear alternate 100;
    //  background: red;
    //  border-radius: 50%;
    //}
    //@keyframes run-around {
    //  0% {
    //    transform: translate(0, 0);
    //  }
    //  25% {
    //    transform: translate(200px, 0);
    //  }
    //  50% {
    //    transform: translate(200px, 200px);
    //  }
    //  75% {
    //    transform: translate(0, 200px);
    //  }
    //}

</style>
<div class="ball-running"></div>

參考:

transform和left改變位置的效能區別
GPU Accelerated Compositing in Chrome
瀏覽器層合成與頁面渲染優化
瀏覽器渲染頁面過程與頁面優化
無線效能優化:Composite
瀏覽器渲染過程
渲染流程
Stick to Compositor-Only Properties and Manage Layer Count

相關文章