這里給大家分享我在網上總結出來的一些知識,希望對大家有所幫助

大家可能經常會聽到 css 影片比 js影片性能更好這樣的論斷,或者是“硬體加速”,“層提升” 這樣的字眼;要了解這些內容就需要對瀏覽器的渲染流程有個大致的了解,本文就是我個人對這些內容的一個總結梳理
需要注意的是:
- 本文僅個人學習總結梳理,如有錯漏,望指正
- 本文以谷歌瀏覽器Blink內核為例
- 隨著谷歌瀏覽器的更新迭代,有些渲染流程或物件名詞可能發生變化(如, RenderObject 變成了 LayoutObject,RenderLayer 變成了 PaintLayer),查看相關檔案時需要注意檔案的時間
渲染流程
先來看下blink的一個大致渲染流程,圖源谷歌的一份共享幻燈片 Life of a Pixel ,它比較全面的闡述了瀏覽的渲染流程,非常值得一看,我們就借這張圖來梳理一遍

圖中分為 渲染行程(renderer process) 和 GPU行程(GPU process) 兩部分,其中渲染行程包含 主執行緒(main) 和 合成執行緒(impl)
我們可以借助谷歌開發工具的 performance 標簽查看是否執行了某些渲染流程步驟,我這里寫了一個簡單的html可以作為對比
<!DOCTYPE html>
<html lang="zh-cn">
?
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>transform demo</title>
</head>
<style>
#normal {
display: grid;
place-items: end;
width: 150px;
height: 150px;
background-color: pink;
}
?
#compositor {
margin-top: 20px;
display: grid;
place-items: end;
width: 150px;
height: 150px;
background-color: palegoldenrod;
}
?
#stacking {
display: grid;
place-items: end;
width: 150px;
height: 150px;
position: absolute;
z-index: -1;
top: 240px;
background-color: skyblue;
}
?
.active {
animation: transformAni 2s both;
}
?
@keyframes transformAni {
to {
transform: translate(200px);
}
}
</style>
?
<body>
<div id="compositor">Compositor Layers</div>
<div style="display: flex; margin-top: 20px;">
<div id="cssBtn" style="background-color: palegoldenrod; width: 200px;">add css animation</div>
</div>
?
<div id="stacking">The Stacking Context</div>
<div style="display: flex; margin-top: 220px;">
<div id="jsBtn" style="background-color: skyblue; width: 200px;"> add js animation</div>
</div>
?
<script>
const cssBtn = document.getElementById('cssBtn')
const compositor = document.getElementById('compositor')
cssBtn.addEventListener('click', () => {
compositor.classList.add("active");
})
?
const jsBtn = document.getElementById('jsBtn')
const stacking = document.getElementById('stacking')
jsBtn.addEventListener('click', () => {
setInterval(() => {
stacking.style.left = `${stacking.getBoundingClientRect().left + 10}px`
}, 100);
})
?
</script>
</body>
?
</html>

1. 構建DOM樹
對應頭圖中 DOM 節點,由于瀏覽器本身無法直接理解和使用html,所以需要將html轉換為瀏覽器能夠理解的DOM樹,也正是因此我們才能通過js控制dom節點

2. 樣式計算
對應頭圖中 style 節點,不僅是html,瀏覽器同樣無法直接讀懂我們寫的 css ,因此瀏覽器會將我們寫的 css 轉換成它能理解的 styleSheets ,同時計算每個 DOM 節點的樣式結果,包括將處理樣式的繼承覆寫,將 rem 等相對單位轉換成 px,將 margin: 8 這樣的縮寫,拆開決議成 margin-left: 8,margin-top: 8 等具體的值,可以通過 computed 標簽查看,

3. 布局計算
對應頭圖中 layout 節點,這個階段也是我們很常聽到的 回流(reflow),重排,在上兩個階段結束后會生成一個儲存其計算結果的樹結構 LayoutObject Tree,在這個階段瀏覽器會遍歷 LayoutObject Tree 計算每個節點在頁面上具體的布局(比如是正常流布局,或是flex布局,哪個元素該放到哪個具體的像素位置上),計算文本實際寬高等;這一階段谷歌正在重構,目前輸入和輸出都混在 LayoutObject Tree 上,之后可能會將輸出部分抽離出來
4. 分層階段
對應頭圖中 comp.assign (compositing assignments) 節點,這個階段是我們獲取性能提升的關鍵,頁面上的元素,根據所處坐標空間(基本可以理解為層疊背景關系)不同等原因,會被劃分為不同的 PaintLayer,通過分層的方式保證頁面上元素以正確的順序層疊;在此基礎上,某些特殊的PaintLayer 會被提升為合成層(Compositing Layers),每個合成層擁有單獨的 GraphicsLayer , 而沒有被提升的 PaintLayer 則與其祖先元素共用同一個 GraphicsLayer.
它們間的對應關系如下圖

每個 GraphicsLayer 都有一個 GraphicsContext,GraphicsContext 負責輸出該層的位圖,即每層代表一份位圖,GPU將位圖合成渲染到螢屏上也就是我們看到的頁面
我們可以通過開發者工具的 Layer 標簽看到 GraphicsLayer 的分層,劃分 PaintLayer 和 提升為 GraphicsLayer 的條件具體可見 無線性能優化:Composite (需要注意層重疊,層壓縮問題)
比如我上面的例子中,我給橙色的 div 加上了 will-change:transform 導致了層提升,而藍色的 div 與 document 共用一個 GraphicsLayer;我們還可以在 Details 標簽看到層提升的具體原因還有記憶體消耗 (tips: 層提升原因還可以看 safari 瀏覽器開發者工具的 layers ,會更加具體)

5. Pre-paint
這一階段主要有兩個任務,一是判斷與上一次paint階段(見下)相比有哪些內容需要被更新,二是構建 property trees
Paint invalidation which invalidates display items which need to be painted. Builds paint property trees.
property trees 的 property 是指 translation, scale 等需要大量計算的屬性,將這些屬性抽離出來單獨管理,避免父元素的變動導致其子元素上所有的屬性都有全部重新計算,具體見 How cc Works
6. paint
繪制階段,這一階段即我們常說的重繪階段,但這一階段并不是執行實際的頁面繪制,而是依據頁面內容的層疊順序生成 繪制任務串列,詳見 layer 工具,滾動滑輪可以重播繪制程序,可以觀察到,同一層疊背景關系情況下,先生成背景繪制任務,再生成元素內容繪制任務,再生成更高層級的層疊背景關系元素的繪制任務;
主執行緒的任務到這里基本結束,將繪制串列提交(commit)到合成執行緒
7. tiling
tiling 分塊,為 GPU光柵化做準備;光柵化是GPU根據繪制任務生成位圖,并將位圖儲存在記憶體中,大家可能聽過 CPU 光柵化的操作,這里參考一段 How cc Works 中文譯文
Chromium 目前實際支持三種不同的光柵化和合成的組合方式:軟體光柵化 + 軟體合成,軟體光柵化 + gpu 合成,gpu 光柵化 + gpu 合成,在移動平臺上,大部分設備和移動版網頁使用的都是 gpu 光柵化 + gpu 合成的渲染方式,理論上性能也最佳
由于這一操作需要消耗較多資源,為了減少資源消耗和使頁面更快呈現會將圖層進行分塊( tiles ),將圖塊作為光柵化的基本單位,同時優先對視口附近的圖塊進行光柵化
通過rendering 標簽,勾選 layer borders 可以看到分塊情況,橙線是不同的 layer 而 青綠色的線則劃分了圖塊

8. raster
這一步由GPU執行光柵化操作,之后的節點我沒再深入了解,大概是光柵化生成draw quads 命令,該命令會參考光柵化結果最后將內容展現在螢屏上
總結
最后我們分別錄制兩個影片的執行流程
js 影片
可以看到 js 影片在每次執行時會重排重繪,執行整個流程,上面橙紅色的那條前面有寫到 Layout Shift,即 布局提升,也就是我們說的強制重排,因為我們在 js 腳本里執行了 stacking.getBoundingClientRect().left 訪問元素位置,這就需要立刻重排來計算元素當前的位置
css影片

可以看到,css影片主執行緒上沒有進行重排重繪
梳理完整個流程,我們就能理解開頭提到的內容了,關鍵點就在于分層合成
“層提升” 即文中的 分層階段;
“硬體加速” 即 GPU加速,一些可能導致頁面大范圍重排重繪(如 translate影片),或需要大量簡單計算的任務(如 filter影片)都會導致層提升,將這部分任務交由GPU處理,將處理完后的結果再合成到頁面上;
而 css 影片性能更優的原因是:
- 避免了通過js訪問元素的位置資訊導致強制重排
- css影片元素移動時在合成層上進行,避免了頁面重排
- 合成由 GPU 行程控制,即使 js 阻塞主執行緒,css影片也能正常執行
層提升會加大記憶體消耗,加大移動端設備負擔,需要酌情使用
補充
will-change
上文我們的例子提到了 will-change 屬性,它的作用是提前告知瀏覽器可能變動的屬性,讓瀏覽器提前做好準備,提前進行相關計算等,它有以下取值
auto讓瀏覽器自己猜哪些值會變動scroll-position表示滾動條位置可能發生變化或產生影片contents表示元素內容可能變動或產生影片<custom-ident>表示所有css屬性
基本上哪里的css屬性變化導致了頁面的卡頓都可以使用 will-change 優化
我們的例子中已經寫入了 will-change: transform ,因此瀏覽器一開始就幫我們做了層提升準備,所以橙色 div 一開始在頁面上就是分層的情況,而如果我們去掉這個屬性,觀察 layer 會發現橙色 div 一開始在頁面上并沒有層提升,只有在執行影片時才進行了層提升,影片結束后層提升又消失了
使用該屬性同樣要注意的是記憶體消耗問題,因為瀏覽器會提前進行優化計算并儲存計算結果,由于瀏覽器本身已經做了十足的性能優化,因此在頁面沒出現影片卡頓之前沒有必要使用該屬性,如果需要使用也盡量通過以下形式:
.will-change-parent:hover .will-change {
will-change: transform;
}
.will-change {
transition: transform 0.3s;
}
.will-change:hover {
transform: scale(1.5);
}
當父元素 hover 時,給子元素加上 will-change,hover 失效則移出,既給了瀏覽器準備的時間,又避免了一直掛著該屬性帶來的資源消耗
requestAnimationFrame / requestIdleCallback
講到影片我們就順便提一嘴 requestAnimationFrame 和 requestIdleCallback
我們看到的影片都是由螢屏快速播放一系列連貫的圖片組成,為了讓人眼感受不到卡頓,大多數螢屏的重繪頻率都是60Hz,即一秒鐘重繪六十次螢屏,每次重繪叫做一幀,一幀時間大約16.7ms,如果一幀的渲染時間超過這個數就會導致影片看起來出現了卡頓,一幀流程大致如下圖

requestAnimationFrame會在每一幀的渲染流程執行前都執行一次,因此使用js實作影片時,相比于 setInterval 實際執行時間的不確定性requestAnimationFrame 更加可靠;
而 requestIdleCallback 則是在每一幀結束前判斷是否有剩余時間,如果有則執行,無則不執行
本文轉載于:
https://juejin.cn/post/7116819495628472327
如果對您有所幫助,歡迎您點個關注,我會定時更新技術檔案,大家一起討論學習,一起進步,

轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/498860.html
標籤:Html/Css
