瀏覽器渲染簡述

singsong發表於2019-03-04

singsong: 文字是自己看了一些不錯資料整理出來的,對該知識點感興趣的同學可以檢視參考文章小節。

✏️最新內容請以github上的為準❗️

為什麼要寫這篇文章?

主要為 CSS 優化工作打一下基礎。要編寫高效能的網站和應用,除了確保編寫的程式碼儘可能高效地執行外,還需要確保頁面效能,重新整理頻率儘量到達 60FPS。這就需要了解瀏覽器是如何進行渲染的。而瀏覽器渲染與 CSS 密切相關,因此只有瞭解其中工作原理才能讓 CSS 更好地工作。

另外,接下來會出一篇優化實戰文章,會涉及 JavaScript 和 CSS 一些優化。其中關於 JavaScript 的優化之前已進行過介紹:常見的 JavaScript 記憶體洩露。本文是對 CSS 優化進行一個補充。

Contents

  • 瀏覽器
  • DOM tree
  • CSSOM tree
  • RenderObject tree(也稱為 Render tree)
  • Layout(佈局)
  • RenderLayer tree
  • Rendering(渲染方式)
  • GrphicsLayer tree
  • Tiled Rendering(瓦片渲染)
  • High Performance Animations(流暢動畫)
  • 總結
  • 參考文章

瀏覽器

瀏覽器渲染簡述
  1. 使用者介面(User Interface):包括位址列、前進/後退按鈕、書籤選單等。除了瀏覽器主視窗顯示的您請求的頁面外,其他顯示的各個部分都屬於使用者介面。
  2. 瀏覽器引擎(Browser engine):在使用者介面(User Interface)和渲染引擎(Rendering engine)之間傳送指令。
  3. 渲染引擎(Rendering engine):負責顯示請求的內容。如果請求的內容是 HTML,它就負責解析 HTML 和 CSS 內容,並將解析後的內容顯示在螢幕上。
  4. 網路(Networking):用於網路呼叫,比如 HTTP 請求。其介面與平臺無關,併為所有平臺提供底層實現。
  5. JavaScript 直譯器(JavaScript Interperter):用於解析和執行 JavaScript 程式碼。
  6. 使用者介面後端(UI Backend):用於繪製基本的視窗小部件,比如組合框和視窗。其公開了與平臺無關的通用介面,而在底層使用作業系統的使用者介面方法。
  7. 資料儲存(Data storage):這是持久層。瀏覽器需要在硬碟上儲存各種資料,例如 Cookies。瀏覽器還支援諸如 localStorage,IndexedDB,WebSQL 和 FileSystem 之類的儲存機制。

本文主要介紹瀏覽器的渲染,即渲染引擎(Rendering engine)負責的工作: 將請求的 HTML 和 CSS 內容解析並渲染在螢幕上。

DOM tree

DOM:Document Object Model,文件物件模型。它可以以一種獨立於平臺和語言的方式,訪問和修改一個文件的內容和結構。它定義了一組與平臺,語言無關的介面,該介面允許程式語言動態地訪問和修改結構化文件。基於 DOM 表示的文件被描述成一個樹形結構,使用 DOM 的介面可以對 DOM 樹進行操作。

這裡以一個例項展開講解,如下 HTML 結構包含一些文字和一張圖片:

<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link href="style.css" rel="stylesheet">
    <title>Critical Path</title>
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg"></div>
  </body>
</html>
複製程式碼

瀏覽器是如何處理這個 HTML 頁面:

瀏覽器渲染簡述
  1. 轉換(Conversion):瀏覽器從磁碟或網路讀取 HTML 的位元組碼,並根據檔案的指定編碼(例如 UTF-8)將其轉換成對應的字元。
  2. 令牌化(Tokenizing):瀏覽器將字串轉換成W3C HTML5 標準規定的各種 token,例如<html><body>,以及其他尖括號內的字串。每個 token 都具有一定特殊含義和規則。
  3. 詞法分析(Lexing):將 token 轉換成定義其屬性和規則的“物件”。
  4. DOM tree:HTML 標籤定義了不同標籤之間的關係(某些標籤包含在其他標籤中),所建立的物件連結在樹狀資料結構中,該資料結構還捕獲原始標籤中定義的父——子關係:HTML 是 body 的父物件,body 是段落的父物件,依此類推。

整個過程的最終輸出是 HTML 頁面的 DOM tree,後續瀏覽器對頁面進一步的所有處理都會用到它。瀏覽器每次處理 HTML 標籤時,都會完成以上所有步驟:將位元組轉換成字元,確定 token,將 token 轉換成節點,然後構建 DOM tree

CSSOM tree

CSSOM:CSS Object Model,CSS 物件模型。CSSOM 定義了 JavaScript 訪問樣式的能力和方式。它是在 DOM 中的一些介面中,加入獲取和操作 CSS 屬性或介面的 JavaScript 介面,因而 JavaScript 可以動態操作 CSS 樣式。DOM 提供了介面讓 JavaScript 修改 HTML 文件,CSSOM 提供了介面讓 JavaScript 獲得和修改 CSS 程式碼設定的樣式資訊。

在瀏覽器構建 DOM 遇到 link 標籤時,該標籤引用一個外部 CSS 樣式表:style.css。由於預見到需要利用該資源來渲染頁面,它會立即發出對該資源的請求,並返回以下內容:

body { font-size: 16px }
p { font-weight: bold }
span { color: red }
p span { display: none }
img { float: right }
複製程式碼

與處理 HTML 時類似,需要將收到的 CSS 規則轉換成某種瀏覽器能夠理解和處理的內部表示。因此會重複 HTML 過程,不過是對 CSS 而不是 HTML:

瀏覽器渲染簡述

將 CSS 位元組轉換成字元,接著轉換成 token 和節點,最後將它們連結到 CSSOM tree 結構中:

cssom-tree.png

CSSOM tree 可以用於確定節點物件的計算樣式。如 span 標記包含了color:red樣式和繼承於 body 標記的font-size:16px樣式;

RenderObject tree(也稱為 Render tree)

  • DOM tree 中,存在不可見與可見節點之分。顧名思義,不可見節點是不需要繪製最終頁面中的節點,如metaheadscript等,以及通過 CSS 樣式display:none隱藏的節點。相反可見節點是使用者可見的,如bodydivspancanvasimg等。對於這些可見節點,瀏覽器需要將它們的內容繪製到最終的頁面中,所以瀏覽器會為它們建立對應的 RenderObject 物件。一個 RenderObject 物件儲存了為繪製 DOM 節點的各種資訊。這些 RenderObject 物件與 DOM 物件類似,也構成一棵樹,稱為RenderObject tree。RenderObject tree 是基於 DOM tree 建立起來的一棵新樹,是為了佈局計算和渲染等機制而構建的一種新的內部表示。RenderObject tree 節點與 DOM tree 節點不是一一對應關係。因為建立一個 RenderObject 物件需要滿足如下規則:

  • DOM tree 的 document 節點

  • DOM tree 中的可見節點,如htmlbodydiv等。而瀏覽器不會為不可見節點建立 RenderObject 節點。

  • 某些情況下瀏覽器需要建立匿名的 RenderObject 節點,該節點不對應 DOM 樹中的任何節點。

RenderObject 物件構成了 RenderObject tree,每個 RenderObject 物件儲存了為繪製 DOM 節點的計算樣式。RenderObject tree 也可以理解成由 CSSOM treeDOM tree 合併成:

瀏覽器渲染簡述

Layout(佈局)

當瀏覽器建立 RenderObject 物件之後,每個物件並不知道自己在裝置視口內的位置、大小等資訊。瀏覽器根據盒模型(Box-model)來計算它們的位置、大小等資訊的過程稱為佈局計算(重排)。佈局計算是一個遞迴的過程,這是因為一個節點的大小通常需要先計算它的孩子節點的位置、大小等資訊。為了計算節點在頁面中的確切大小和位置,瀏覽器會從 RenderObject tree 的根節點開始進行遍歷。

例項:

<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>Critial Path: Hello world!</title>
  </head>
  <body>
    <div style="width: 50%">
      <div style="width: 50%">Hello world!</div>
    </div>
  </body>
</html>
複製程式碼

頁面包含了兩個巢狀 div:父 div 將其的顯示尺寸設定為 viewport 寬度的 50%,子 div 將其寬度設定為其父項的 50%,即 viewport 寬度的 25%。

瀏覽器渲染簡述

RenderLayer tree

瀏覽器渲染引擎並不是直接使用 RenderObject tree 進行繪製,為了方便處理 Positioning(定位),Clipping(裁剪),Overflow-scroll(頁內滾動),CSS Transform/Opacity/Animation/Filter,Mask or Reflection,Z-indexing(Z 排序)等,瀏覽器需要會為一些特定的 RenderObject 生成對應的 RenderLayer,並生成一棵對應的 RenderLayer tree。而這些特定的 RenderObject 跟對應的 RenderLayer 就是直屬的關係,如果它們的子節點如果沒有對應的 RenderLayer,就從屬於父節點的 RenderLayer。最終,每一個 RenderObject 都會直接或者間接地從屬於一個 RenderLayer。因此 RenderObject 節點與 RenderLayer 節點不是一一對應關係,而是一對多的關係。那需要滿足什麼條件,渲染引擎才為 RenderObject 建立對應的 RenderLayer:

  • It`s the root object for the page
  • It has explicit CSS position properties (relative, absolute or a transform)
  • It is transparent
  • Has overflow, an alpha mask or reflection
  • Has a CSS filter
  • Corresponds to <canvas> element that has a 3D (WebGL) context or an accelerated 2D context
  • Corresponds to a <video> element

翻譯:

  • DOM tree 的 Document 節點對應的 RenderObject 節點和 HTML 節點對應的 RenderObject 節點
  • 顯式指定 CSS position 屬性的 RenderObject 節點
  • 有透明度的 RenderObject 節點
  • 有 overflow,alpha 和 reflection 的樣式 RenderObject 節點
  • 有 filter 樣式的 RenderObject 節點
  • 使用 Canvas 2D 和 3D(WebGL)技術的 RenderObject 節點
  • video 元素對應的 RenderObject 節點

每個 RenderLayer 物件可以想象成影像中一個圖層,各個圖層疊加構成了一個影像。瀏覽器會遍歷 RenderLayer tree,再遍歷從屬這個 RenderLayer 的 RenderObject,RenderObject 物件儲存有繪製資訊,並進行繪製。RenderLayer 和 RenderObject 共同決定了最終呈現的網頁內容,RenderLayer tree 決定了網頁的繪製的層次順序,而從屬於 RenderLayer 的 RenderObject 決定了該 RenderLayer 的內容。

Rendering(渲染方式)

在完成構建 RenderLayer tree 之後,瀏覽器會使用圖形庫將其構建的渲染模型繪製出來,該過程分為兩個階段:

  • 繪製:將從屬每個 RenderLayer 圖層上的 RenderObject 繪製在其 RenderLayer 上。即繪製(Paint)或者光柵化(Rasterization),將一些繪圖指令轉換成真正的畫素顏色值。
    • 軟體繪圖:CPU 來完成繪圖操作
    • 硬體加速繪圖:GPU 來完成繪圖操作
  • 合成(compositing):將各個 RenderLayer 圖層合併成到一個點陣圖(Bitmap)中。同時還可能包括位移(Translation),縮放(Scale),旋轉(Rotation),Alpha 合成等操作。

渲染引擎的渲染,目前有三種網頁的渲染方式:

  • 硬體加速合成(Accelerated Compositing):使用 GPU 來完成合成工作。
  • 合成化渲染:使用合成(compositing)技術的渲染稱。
  • 軟體渲染方式:使用 CPU 來繪製每個 RenderLayer 圖層的內容(RenderObject)到一個點陣圖,即一塊 CPU 使用的記憶體空間。繪製每一層的時候都會使用該點陣圖,區別在於繪製的位置可能不一樣,繪製順序按照從後到前。因此軟體渲染機制是沒有合成階段的。
  • 硬體加速渲染的合成化渲染方式:使用 GPU 來繪製所有合成層,並使用 GPU 硬體來加速合成。
  • 軟體繪圖的合成化渲染方式: 某些合成層使用 CPU 來繪圖,另外一些使用 GPU 來繪製。對於使用 CPU 來繪製的圖層,該層的繪製結果會先儲存在 CPU 記憶體中,之後會被傳輸到 GPU 記憶體中,然後再使用 GPU 來完成合成工作。
瀏覽器渲染簡述

第二種和第三種渲染方式,都是使用了合成化渲染技術,合成工作也都是由 GPU 來做。對於常見的 2D 繪圖操作,使用 GPU 來繪圖不一定比使用 CPU 繪圖在效能上有優勢,例如繪製文字、點、線等。原因是 CPU 的使用緩衝機制有效減少了重複繪製的開銷而且不需要考慮與 GPU 並行。另外,GPU 的記憶體資源相對 CPU 的記憶體資源來說比較緊張,而且網頁的分層使得 GPU 的記憶體使用相對比較多。鑑於此,就目前的情況來看,三者都存在是有其合理性的,下面分析一下它們的特點:

  • 軟體渲染是目前很常見的技術,也是瀏覽器最早使用的渲染方式。這一技術比較節省記憶體,特別是寶貴的 GPU 記憶體,但是軟體渲染只能處理 2D 方面的操作。簡單的網頁沒有複雜繪圖或者多媒體方面的需求,軟體渲染方式就比較合適來渲染該型別的網頁。問題是,一旦遇上了 HTML5 的很多新技術,軟體渲染顯得無能為力,一是因為能力不足;二是因為效能不好,例如視訊、Canvas 2D 等。所以,軟體渲染技術被用的越來越少,特別是在移動領域。軟體渲染同硬體加速渲染另外一個很不同的地方就是對更新區域的處理。當網頁中有一個更新小型區域的請求(如動畫)時,軟體渲染可能只需要計算一個極小的區域,而硬體渲染可能需要重新繪製其中的一層或者多層,然後再合成這些層。硬體渲染的代價可能會大得多。
  • 對於硬體加速的合成化渲染方式來說,每個層的繪製和所有層的合成均使用 GPU 硬體來完成,這對需要使用 3D 繪圖的操作來說特別合適。這種方式下,在 RenderLayer 樹之後,瀏覽器還需要建立更多的內部表示,目的是支援硬體加速機制,這顯然會消耗更多的記憶體資源。但是,一方面,硬體加速機制能夠支援現在所有的 HTML5 定義的 2D 或者 3D 繪圖示準;另一方面,關於更新區域的討論,如果需要更新某個層的一個區域,因為軟體渲染沒有為每一層提供後端儲存,因而它需要將和這個區域有重疊部分的所有層次的相關區域一次從後往前重新繪製一遍,而硬體加速渲染只需要重新繪製更新發生的層次,因而在某些情況下,軟體渲染的代價又變得更大。當然,這取決於網頁的結構和渲染策略。
  • 軟體繪圖的合成化渲染方式結合了前面兩種方式的優點,這時因為很多網頁可能既包含基本的 HTML 元素,也包含一些 HTML5 新功能,使用 CPU 繪圖方式來繪製某些層,使用 GPU 來繪製其他一些層。原因當然是前面所述的基於效能和記憶體方面綜合考慮的結果。

瀏覽器還可以使用多執行緒的渲染架構,將網頁內容繪製到後端儲存的操作放到另外一個獨立的執行緒(繪製執行緒),而原來執行緒轉為合成執行緒,繪製執行緒跟合成執行緒之間可以使用同步,部分同步,完全非同步等作業模式,讓瀏覽器可以在效能與效果之間根據需要進行選擇。

GrphicsLayer tree

對於軟體渲染而言,到 RenderLayer tree 就結束了,後面不會建立其它額外的樹來對應於 RenderLayer tree。但是,對於硬體渲染來說,在 RenderLayer tree 之後,瀏覽器渲染引擎為硬體渲染提供了更多的內部結構來支援這個機制。

在硬體加速渲染的合成化渲染和軟體繪圖的合成化渲染架構下,一個 RenderLayer 物件如果需要後端儲存,它會建立一個 RenderLayerBacking 物件,該物件負責 Renderlayer 物件所需要的各種儲存。理想情況下,每個 RenderLayer 都可以建立自己的後端儲存,事實上不是所有 RenderLayer 都有自己的 RenderLayerBacking 物件。如果一個 RenderLayer 物件被像樣的建立後端儲存,那麼將該 RenderLayer 稱為合成層(Compositing Layer)。

哪些 RenderLayer 物件可以是合成層?如果一個 RenderLayer 物件具有以下的特徵之一,那麼它就是合成層:

  • Layer has 3D or perspective transform CSS properties
  • Layer is used by < video> element using accelerated video decoding
  • Layer is used by a < canvas> element with a 3D context or accelerated 2D context
  • Layer is used for a composited plugin
  • Layer uses a CSS animation for its opacity or uses an animated webkit transform
  • Layer uses accelerated CSS filters
  • Layer with a composited descendant has information that needs to be in the composited layer tree, such as a clip or reflection
  • Layer has a sibling with a lower z-index which has a compositing layer (in other words the layer is rendered on top of a composited layer)

翻譯:

  • RenderLayer 具有 3D 或透視轉換的 CSS 屬性
  • RenderLayer 包含使用硬體加速的視訊解碼技術的<video>元素
  • RenderLayer 包含使用硬體加速的 2D 或 WebGL-3D 技術的<canvas>元素
  • RenderLayer 使用了合成外掛。
  • RenderLayer 使用了opacitytransform動畫
  • RenderLayer 使用了硬體加速的 CSS Filters 技術
  • RenderLayer 後代中包含了一個合成層(如有 clip 或 reflection 屬性)
  • RenderLayer 有一個 z-index 比自己小的合成層(即在一個合成層之上)

每個合成層都有一個 RenderLayerBacking,RenderLayerBacking 負責管理 RenderLayer 所需要的所有後端儲存,因為後端儲存可能需要多個儲存空間。在瀏覽器(WebKit)中,儲存空間使用類 GraphicsLayer 來表示。瀏覽器會為這些 RenderLayer 建立對應的 GraphicsLayer,不同的瀏覽器需要提供自己的 GrphicsLayer 實現用於管理儲存空間的分配,釋放,更新等等。擁有 GrphicsLayer 的 RenderLayer 會被繪製到自己的後端儲存,而沒有 GrphicsLayer 的 RenderLayer 它們會向上追溯有 GrphicsLayer 的父/祖先 RenderLayer,直到 Root RenderLayer 為止,然後繪製在有 GrphicsLayer 的父/祖先 RenderLayer 的儲存空間,而 Root RenderLayer 總是會建立一個 GrphicsLayer 並擁有自己獨立的儲存空間。在將每個合成圖層包含的 RenderLayer 內容繪製在合成層的後端儲存中,這裡繪製可以是軟體繪製或硬體繪製。接著由合成器(Compositor)將多個合成層合成起來,形成終端使用者可見的網頁,實際上就是一張圖片。

GraphicsLayer 又構成了一棵與 RenderLayer 並行的樹,而 RenderLayer 與 GraphicsLayer 的關係有些類似於 RenderObject 與 RenderLayer 之間的關係。如下是 DOM treeRenderObject treeRenderLayer treeGraphicsLayer tree關係圖:

瀏覽器渲染簡述

這樣可以合併一些 RenderLayer 層,從而減少記憶體的消耗。其次,合併之後,減少了合併帶來的重繪效能和處理上的困難。在硬體加速渲染的合成化渲染和軟體繪圖的合成化渲染架構下,RenderLayer 的內容變化,只需要更新所屬的 GraphicsLayer 的快取即可,而快取的更新,也只需要繪製直接或者間接屬於這個 GraphicsLayer 的 RenderLayer,而不是所有的 RenderLayer。特別是一些特定的 CSS 樣式屬性的變化,實際上並不引起內容的變化,只需要改變一些 GraphicsLayer 的混合引數,然後重新混合即可,而混合相對繪製而言是很快的,這些特定的 CSS 樣式屬性我們一般稱之為是被加速的,不同的瀏覽器支援的狀況不太一樣,但基本上 CSS Transform & Opacity 在所有支援混合加速的瀏覽器上都是被加速的。被加速的 CSS 樣式屬性的動畫,就比較容易達到 60 幀/每秒的流暢效果了。

瀏覽器渲染簡述

不過並不是擁有獨立快取的 RenderLayer 越多越好,太多擁有獨立快取的 RenderLayer 會帶來一些嚴重的副作用:

  • 它大大增加了記憶體的開銷,這點在移動裝置上的影響更大,甚至導致瀏覽器在一些記憶體較少的移動裝置上無法很好地支援圖層合成加速;
  • 它加大了合成的時間開銷,導致合成效能的下降,而合成效能跟網頁滾動/縮放操作的流暢度又息息相關,最終導致網頁滾動/縮放的流暢度下降,讓使用者覺得操作不夠流暢。

Tiled Rendering(瓦片渲染)

通常一個合成層的後端儲存被分割成多個大小相同的瓦片狀的小儲存空間,每個瓦片可以理解為 OpenGL 中的一個紋理,合成層的結果被分開儲存在這些瓦片中。為什麼使用瓦片化的後端儲存?

  • DOM 樹種的 html 元素所在的層可能會比較大,因為網頁的高度很大,如果只是使用一個後端儲存的話,那麼需要一個很大的紋理物件,但是實際的 GPU 硬體可能只支援非常有限的紋理大小。
  • 在一個比較大的合成層中,可能只是其中一部分發生變化,根據之前的介紹,需要重新繪製整個層,這樣必然產生額外的開銷,使用瓦片話的後端儲存,就只需要重繪一些存在更新的瓦片。
  • 當層發生滾動的時候,一些瓦片可能不再需要,然後渲染引擎需要一些新的瓦片來繪製新的區域,這些大小相同的後端儲存很容易重複利用。
瀏覽器渲染簡述

High Performance Animations(流暢動畫)

網頁載入後,繪製新的每一幀,一般都需要經過計算佈局(layout)、繪圖(paint)、合成(composite)三階段。因此要想提高頁面效能(或 FPS),需要減少每一幀的時間。而在這三個階段中,layout 和 paint 比較耗時間,而合成需要的時間相對較少一些。

layout

如果修改 DOM 元素的 layout 樣式(如 width, heihgt 等),瀏覽器會計算頁面需要 relayout 的元素,然後觸發一個 relayout。被 relayout 的元素,接著會執行繪製,最後進行渲染合併生成頁面。

瀏覽器渲染簡述

paint

如果修改 DOM 元素的 paint 樣式(如 color, background 等),瀏覽器會跳過佈局,直接執行繪製,再進行合成。

瀏覽器渲染簡述

composite

如果修改 DOM 元素的 composite 樣式(如 transform, opacity 等)。瀏覽器會跳過佈局和繪製,直接執行合成。該過程是開銷最小的,也是優化著手點。

瀏覽器渲染簡述

如果想知道修改任何指定 CSS 樣式會觸發 layout、paint、composite 中的哪一個,請檢視CSS 觸發器

優化

可以通過什麼途徑進行優化,減少每一幀的時間(避免過多 layout 或 paint):

  • 使用適合的網頁分層技術減少 layout 和 paint。一旦有請求更新,如果沒有分層,渲染引擎可能需要重新繪製所有區域,因為計算更新部分對 GPU 來說可能消耗更多的時間。而網頁分層之後,部分割槽域的更新可能只在網頁的一層或幾層,而不需要將整個網頁重新繪製。通過重新繪製網頁的一層或幾層,並將它們和其他之前繪製完的層合併起來,既能使用 GPU 的能力,又能夠減少重繪的開銷。
  • 使用合成屬性樣式(opcity、tansform)來完成 tansition 或 animation。當合成器合成時候,每個合成層都可以設定變形屬性:位移(Translate)、縮放(Scale)、旋轉(Rotation)、opacity,這些屬性僅僅改變合成層的變換引數,而不需要 layout 和 paint 操作,極大地減少每一幀的渲染時間。

即使用 GPU 硬體加速,為某些 RenderLayer 建立對應 GraphicsLayer。通過為每一個合成層設定transform屬性來完成 transition 或 animation,有效地避免 relayout 和 repaint 的開銷。

瀏覽器渲染簡述

總結

本文重點介紹了瀏覽器渲染引擎的渲染過程,涉及了 DOM treeCSSOM treeRenderObject treeRenderLayer treeGraphicsLayer tree。並對各種渲染模式進行了簡單介紹,其中引入了硬體加速機制,還給出一些優化建議。瞭解這些知識點對我們開發高效能的 web 應用會有很大的幫助。

參考文章:

相關文章