引用
瀏覽器工作原理與實踐
簡單回顧下上節前三個階段的主要內容:在HTML頁面內容被提交給渲染引擎之后,渲染引擎首先將HTML解析為瀏覽器可以理解的DOM;然后根據CSS樣式表,計算出DOM樹所有節點的樣式;接著又計算每個元素的幾何坐標位置,并將這些信息保存在布局樹中
一、分層
現在我們有了布局樹,而且每個元素的具體位置信息都計算出來了,那么接下來是不是就要開始著手繪制頁面了?
答案依然是否定的。
因為頁面中有很多復雜的效果,如一些復雜的3D變換、頁面滾動,或者使用z-indexing做z軸排序等,為了更加方便地實現這些效果,渲染引擎還需要為特定的節點生成專用的圖層,并生成一棵對應的圖層樹(LayerTree)。如果你熟悉PS,相信你會很容易理解圖層的概念,正是這些圖層疊加在一起構成了最終的頁面圖像。
要想直觀地理解什么是圖層,你可以打開Chrome的“開發者工具”,選擇“Layers”標簽,就可以可視化頁面的分層情況,如下圖所示
從上圖可以看出,渲染引擎給頁面分了很多圖層,這些圖層按照一定順序疊加在一起,就形成了最終的頁面,你可以參考下圖
現在你知道了瀏覽器的頁面實際上被分成了很多圖層,這些圖層疊加后合成了最終的頁面。下面我們再來看看這些圖層和布局樹節點之間的關系,如文中圖所示:
通常情況下,并不是布局樹的每個節點都包含一個圖層,如果一個節點沒有對應的層,那么這個節點就從屬于父節點的圖層。如上圖中的span標簽沒有專屬圖層,那么它們就從屬于它們的父節點圖層。但不管怎樣,最終每一個節點都會直接或者間接地從屬于一個層。
那么需要滿足什么條件,渲染引擎才會為特定的節點創建新的層呢?通常滿足下面兩點中任意一點的元素就可以被提升為單獨的一個圖層。
第一點,擁有層疊上下文屬性的元素會被提升為單獨的一層。
頁面是個二維平面,但是層疊上下文能夠讓HTML元素具有三維概念,這些HTML元素按照自身屬性的優先級分布在垂直于這個二維平面的z軸上。你可以結合下圖來直觀感受下:
從圖中可以看出,明確定位屬性的元素、定義透明屬性的元素、使用CSS濾鏡的元素等,都擁有層疊上下文屬性。
第二點,需要剪裁(clip)的地方也會被創建為圖層。
不過首先你需要了解什么是剪裁,結合下面的HTML代碼:
<style>div {width: 200;height: 200;overflow:auto;background: gray;}
</style>
<body><div ><p>所以元素有了層疊上下文的屬性或者需要被剪裁,那么就會被提升成為單獨一層,你可以參看下圖:</p><p>從上圖我們可以看到,document層上有A和B層,而B層之上又有兩個圖層。這些圖層組織在一起也是一顆樹狀結構。</p><p>圖層樹是基于布局樹來創建的,為了找出哪些元素需要在哪些層中,渲染引擎會遍歷布局樹來創建層樹(Update LayerTree)。</p> </div>
</body>
在這里我們把div的大小限定為200 * 200像素,而div里面的文字內容比較多,文字所顯示的區域肯定會超出200 * 200的面積,這時候就產生了剪裁,渲染引擎會把裁剪文字內容的一部分用于顯示在div區域,下圖是運行時的執行結果
出現這種裁剪情況的時候,渲染引擎會為文字部分單獨創建一個層,如果出現滾動條,滾動條也會被提升為單獨的層。你可以參考下圖:
所以說,元素有了層疊上下文的屬性或者需要被剪裁,滿足這任意一點,就會被提升成為單獨一層。
二、 圖層繪制
在完成圖層樹的構建之后,渲染引擎會對圖層樹中的每個圖層進行繪制,那么接下來我們看看渲染引擎是怎么實現圖層繪制的?
試想一下,如果給你一張紙,讓你先把紙的背景涂成藍色,然后在中間位置畫一個紅色的圓,最后再在圓上畫個綠色三角形。你會怎么操作呢?
通常,你會把你的繪制操作分解為三步:
- 制藍色背景;
- 在中間繪制一個紅色的圓;
- 再在圓上繪制綠色三角形
渲染引擎實現圖層的繪制與之類似,會把一個圖層的繪制拆分成很多小的繪制指令,然后再把這些指令按照順序組成一個待繪制列表,如下圖所示:
從圖中可以看出,繪制列表中的指令其實非常簡單,就是讓其執行一個簡單的繪制操作,比如繪制粉色矩形或者黑色的線等。而繪制一個元素通常需要好幾條繪制指令,因為每個元素的背景、前景、邊框都需要單獨的指令去繪制。所以在圖層繪制階段,輸出的內容就是這些待繪制列表。
你也可以打開“開發者工具”的“Layers”標簽,選擇“document”層,來實際體驗下繪制列表,如下圖所示:
在該圖中,區域1就是document的繪制列表,拖動區域2中的進度條可以重現列表的繪制過程。
# 柵格化(raster)操作
繪制列表只是用來記錄繪制順序和繪制指令的列表,而實際上繪制操作是由渲染引擎中的合成線程來完成的。你可以結合下圖來看下渲染主線程和合成線程之間的關系:
如上圖所示,當圖層的繪制列表準備好之后,主線程會把該繪制列表提交(commit)給合成線程,那么接下來合成線程是怎么工作的呢?
那我們得先來看看什么是視口,你可以參看下圖:
通常一個頁面可能很大,但是用戶只能看到其中的一部分,我們把用戶可以看到的這個部分叫做視口(viewport)。
在有些情況下,有的圖層可以很大,比如有的頁面你使用滾動條要滾動好久才能滾動到底部,但是通過視口,用戶只能看到頁面的很小一部分,所以在這種情況下,要繪制出所有圖層內容的話,就會產生太大的開銷,而且也沒有必要。
基于這個原因,合成線程會將圖層劃分為圖塊(tile),這些圖塊的大小通常是256x256或者512x512,如下圖所示:
然后合成線程會按照視口附近的圖塊來優先生成位圖,實際生成位圖的操作是由柵格化來執行的。所謂柵格化,是指將圖塊轉換為位圖。而圖塊是柵格化執行的最小單位。渲染進程維護了一個柵格化的線程池,所有的圖塊柵格化都是在線程池內執行的,運行方式如下圖所示:
通常,柵格化過程都會使用GPU來加速生成,使用GPU生成位圖的過程叫快速柵格化,或者GPU柵格化,生成的位圖被保存在GPU內存中。
相信你還記得,GPU操作是運行在GPU進程中,如果柵格化操作使用了GPU,那么最終生成位圖的操作是在GPU中完成的,這就涉及到了跨進程操作。具體形式你可以參考下圖:
從圖中可以看出,渲染進程把生成圖塊的指令發送給GPU,然后在GPU中執行生成圖塊的位圖,并保存在GPU的內存中。
三、合成和顯示
一旦所有圖塊都被光柵化,合成線程就會生成一個繪制圖塊的命令——“DrawQuad”,然后將該命令提交給瀏覽器進程。
瀏覽器進程里面有一個叫viz的組件,用來接收合成線程發過來的DrawQuad命令,然后根據DrawQuad命令,將其頁面內容繪制到內存中,最后再將內存顯示在屏幕上。
到這里,經過這一系列的階段,編寫好的HTML、CSS、JavaScript等文件,經過瀏覽器就會顯示出漂亮的頁面了。
四、渲染流水線大總結
好了,我們現在已經分析完了整個渲染流程,從HTML到DOM、樣式計算、布局、圖層、繪制、光柵化、合成和顯示。下面我用一張圖來總結下這整個渲染流程:
結合上圖,一個完整的渲染流程大致可總結為如下
- 渲染進程將HTML內容轉換為能夠讀懂的DOM樹結構。
- 渲染引擎將CSS樣式表轉化為瀏覽器可以理解的styleSheets,計算出DOM節點的樣式。
- 創建布局樹,并計算元素的布局信息。
- 對布局樹進行分層,并生成分層樹。
- 為每個圖層生成繪制列表,并將其提交到合成線程。
- 合成線程將圖層分成圖塊,并在光柵化線程池中將圖塊轉換成位圖。
- 合成線程發送繪制圖塊命令DrawQuad給瀏覽器進程。
- 瀏覽器進程根據DrawQuad消息生成頁面,并顯示到顯示器上
# 相關概念
有了上面介紹渲染流水線的基礎,我們再來看看三個和渲染流水線相關的概念——“重排”“重繪”和“合成”。理解了這三個概念對于你后續Web的性能優化會有很大幫助。
# 1. 更新了元素的幾何屬性(重排)
從上圖可以看出,如果你通過JavaScript或者CSS修改元素的幾何位置屬性,例如改變元素的寬度、高度等,那么瀏覽器會觸發重新布局,解析之后的一系列子階段,這個過程就叫重排。無疑,重排需要更新完整的渲染流水線,所以開銷也是最大的
# 2. 更新元素的繪制屬性(重繪)
接下來,我們再來看看重繪,比如通過JavaScript更改某些元素的背景顏色,渲染流水線會怎樣調整呢?你可以參考下圖:
從圖中可以看出,如果修改了元素的背景顏色,那么布局階段將不會被執行,因為并沒有引起幾何位置的變換,所以就直接進入了繪制階段,然后執行之后的一系列子階段,這個過程就叫重繪。相較于重排操作,重繪省去了布局和分層階段,所以執行效率會比重排操作要高一些。
# 3. 直接合成階段
那如果你更改一個既不要布局也不要繪制的屬性,會發生什么變化呢?渲染引擎將跳過布局和繪制,只執行后續的合成操作,我們把這個過程叫做合成。具體流程參考下圖
在上圖中,我們使用了CSS的transform來實現動畫效果,這可以避開重排和重繪階段,直接在非主線程上執行合成動畫操作。這樣的效率是最高的,因為是在非主線程上合成,并沒有占用主線程的資源,另外也避開了布局和繪制兩個子階段,所以相對于重繪和重排,合成能大大提升繪制效率。
至于如何用這些概念去優化頁面,我們會在后面相關章節做詳細講解的,這里你只需要先結合“渲染流水線”弄明白這三個概念及原理就行
五、總結
Chrome的渲染流水線還是相當復雜晦澀,且難以理解,不過Chrome團隊在不斷添加新功能的同時,也在不斷地重構一些子階段,目的就是讓整體渲染架構變得更加簡單和高效,正所謂大道至簡。
六、思考時間
在優化Web性能的方法中,減少重繪、重排是一種很好的優化方式,那么結合文中的分析,你能總結出來為什么減少重繪、重排能優化Web性能嗎?那又有那些具體的實踐方法能減少重繪、重排呢?
- 觸發repaint reflow的操作盡量放在一起,比如改變dom高度和設置margin分開寫,可能會出發兩次重排
- 通過虛擬dom層計算出操作總得差異,一起提交給瀏覽器。之前還用過createdocumentfragment來匯總append的dom,來減少觸發重排重繪次數。