在日常的開發中,通過使用css屬性,做一些動效、動畫時,會發現在頁面有卡頓;
在Android低端機尤為明顯,故需要知道瀏覽器渲染以及優化手段
瀏覽器渲染流程
- 構建 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 軸空間上疊加,最終構成一個完整的設計圖。
對於有位置重疊的元素的頁面,這個過程尤其重要,因為一旦圖層的合併順序出錯,將會導致元素顯示異常。
渲染物件(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 **
來形成合成層,這樣是對原先的佈局沒有影響
示例
其他合成情況
隱式合成
除此之外,在瀏覽器的 Composite 階段,還存在一種隱式合成,部分渲染層在一些特定場景下,會被預設提升為合成層。其實就是元素div之間相互重疊
上圖所示:把position-1
生成合成層,後面的元素會相互重疊,為了層疊上下文展示正確;導致後面的元素直接隱式合成了;
合層壓縮
如上圖,目前沒有遇到
合成爆炸
通過上圖研究,很容易就產生一些不在預期範圍內的合成層,當這些不符合預期的合成層達到一定量級時,就會變成層爆炸。
需要排查是否有重疊,能否通過z-index
來調節層疊順序
優化
優點
- 合成層的點陣圖,會交由 GPU 合成,比 CPU 處理要快得多;
- 當需要 repaint 時,只需要 repaint 本身,不會影響到其他的層;
- 元素提升為合成層後,transform 和 opacity 才不會觸發 repaint,如果不是合成層,則其依然會觸發 repaint。(只有css動畫,才可以,js去控制還是會重排、重繪的)使用 transform 或者 opacity 來實現動畫效果
更新元素幾何屬性(發生重排)
瀏覽器會觸發重新佈局,解析之後的一系列子階段,這個過程就叫重排。無疑,重排需要更新完整的渲染流水線,所以開銷也是最大的
引發重排的情況
- 頁面首次渲染;
- 瀏覽器視窗大小發生變化;
- 元素的內容發生變化;
- 元素的尺寸或者位置發生變化;
- 元素的字型大小發生變化;
- 啟用CSS偽類;
查詢某些屬性或者呼叫某些方法;
- offsetTop、offsetLeft、offsetWidth、offsetHeight
- scrollTop、scrollLeft、scrollWidth、scrollHeight
- clientTop、clientLeft、clientWidth、clientHeight
- width、height
- getComputedStyle()
- getBoundingClientRect()
- 新增或者刪除可見的DOM元素。
更新元素繪製屬性(發生重繪)
如果修改了元素的背景顏色,那麼佈局階段將不會被執行,因為並沒有引起幾何位置的變換,所以就直接進入了繪製階段,然後執行之後的一系列子階段,這個過程就叫重繪。相較於重排操作,重繪省去了佈局和分層階段,所以執行效率會比重排操作要高一些。
引發重繪的情況
- 重排
- visibility: visible <=> hidden
- 顏色改變
- 其他幾何變化...
直接合成階段
我們使用了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