How Rendering Work (in WebKit and Blink)

自從開始從事瀏覽器內核開發工作以來,已經寫過不少跟渲染相關的文章。但是一直想寫一篇像 How Browsers Work 類似,能夠系統,完整地闡述瀏覽器的渲染引擎是如何工作的,它是如何對網頁渲染性能進行優化的文章,卻一直因爲畏懼所需要花費的時間和精力,遲遲無法動筆。不管如何,現在終於鼓起勇氣來寫了,希望自己能夠完成吧…

文章包括的主要內容如下 —

  • 渲染基礎 - DOM & RenderObject & RenderLayer
  • WebView,繪製與混合,多線程渲染
  • 硬件加速
  • 分塊渲染
  • 圖層混合加速
  • 網頁遊戲渲染 - Canvas & WebGL

首先明確文中關於渲染的定義,瀏覽器內核引擎通常又被稱爲網頁渲染引擎,但是這裏的渲染實際上是一個泛指,廣義的渲染,它包括了瀏覽器內核所有的主要工作 - 加載,解析,排版,繪製等等。而在本文裏面的渲染,指的是跟繪製相關的部分,也就是瀏覽器是如何將排版後的結果最終顯示在屏幕上的這一過程。如果讀者希望先對瀏覽器內核引擎,特別是 WebKit 有一個大概的瞭解, How Browsers Work How WebKit Work WebKit for Developers 可以提供不錯的入門指引。

其次,本文主要描述 WebKit 引擎的實現,不過因爲 Blink 實際上從 WebKit 分支出來的時間並不長,兩者在渲染整體架構上還是基本一致的,所以文中不會明確區分這兩者。

最後,希望這篇文章能夠給從事瀏覽器內核開發,特別是渲染引擎開發的開發者一個能夠快速入門的指引,並給前端開發者優化網頁渲染性能提供足夠的知識和幫助。

因爲文章後續還有可能會持續修訂和補充,如果要查看最新的內容,請訪問個人博客的原文:http://blog.csdn.net/rogeryi/article/details/23686609,如果有什麼疏漏和錯誤,也歡迎讀者來信指正([email protected])。

渲染基礎 - DOM & RenderObject & RenderLayer

DOM,RenderObject,RenderLayer

圖片來自 GPU Accelerated Compositing in Chrome

當瀏覽器通過網絡或者本地文件系統加載一個 HTML 文件,並對它進行解析完畢後,內核就會生成它最重要的數據結構 - DOM 樹。DOM 樹上每一個節點都對應着網頁裏面的每一個元素,並且網頁也可以通過 JavaScript 操作這棵 DOM 樹,動態改變它的結構。但是 DOM 樹本身並不能直接用於排版和渲染,內核還會生成另外一棵樹 - Render 樹,Render 樹上的每一個節點 - RenderObject,跟 DOM 樹上的節點幾乎是一一對應的,當一個可見的 DOM 節點被添加到 DOM 樹上時,內核就會爲它生成對應的 RenderOject 添加到 Render 樹上。

Render Tree

圖片來自 How WebKit Work

Render 樹是瀏覽器排版引擎的主要作業對象,排版引擎根據 DOM 樹和 CSS 樣式表的樣式定義,按照預定的排版規則確定了 Render 樹最後的結構,包括其中每一個 RenderObject 的大小和位置,而一棵經過排版的 Render 樹,則是瀏覽器渲染引擎的主要輸入,讀者可以認爲,Render 樹是銜接瀏覽器排版引擎和渲染引擎之間的橋樑,它是排版引擎的輸出,渲染引擎的輸入。

Layer Tree

圖片來自 How WebKit Work

不過瀏覽器渲染引擎並不是直接使用 Render 樹進行繪製,爲了方便處理 Positioning(定位),Clipping(裁剪),Overflow-scroll(頁內滾動),CSS Transform/Opacity/Animation/Filter,Mask or Reflection,Z-indexing(Z排序)等,瀏覽器需要生成另外一棵樹 – Layer 樹。渲染引擎會爲一些特定的 RenderObject 生成對應的 RenderLayer,而這些特定的 RenderObject 跟對應的 RenderLayer 就是直屬的關係,相應的,它們的子節點如果沒有對應的 RenderLayer,就從屬於父節點的 RenderLayer。最終,每一個 RenderObject 都會直接或者間接地從屬於一個 RenderLayer。

RenderObject 生成 RenderLayer 的條件,來自 GPU Accelerated Compositing in Chrome – 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

瀏覽器渲染引擎遍歷 Layer 樹,訪問每一個 RenderLayer,再遍歷從屬於這個 RenderLayer 的 RenderObject,將每一個 RenderObject 繪製出來。讀者可以認爲,Layer 樹決定了網頁繪製的層次順序,而從屬於 RenderLayer 的 RenderObject 決定了這個 Layer 的內容,所有的 RenderLayer 和 RenderObject 一起就決定了網頁在屏幕上最終呈現出來的內容。

軟件渲染模式下,瀏覽器繪製 RenderLayer 和 RenderObject 的順序,來自 GPU Accelerated Compositing in Chrome

In the software path, the page is rendered by sequentially painting all the RenderLayers, from back to front. The RenderLayer hierarchy is traversed recursively starting from the root and the bulk of the work is done in RenderLayer::paintLayer() which performs the following basic steps (the list of steps is simplified here for clarity):

  1. Determines whether the layer intersects the damage rect for an early out.
  2. Recursively paints the layers below this one by calling paintLayer() for the layers in the negZOrderList.
  3. Asks RenderObjects associated with this RenderLayer to paint themselves.
  4. This is done by recursing down the RenderObject tree starting with the RenderObject which created the layer. Traversal stops whenever a RenderObject associated with a different RenderLayer is found.
  5. Recursively paints the layers above this one by calling paintLayer() for the layers in the posZOrderList.

In this mode RenderObjects paint themselves into the destination bitmap by issuing draw calls into a single shared GraphicsContext (implemented in Chrome via Skia).

WebView,繪製與混合,多線程渲染

WebView

圖片來自 [UC 瀏覽器 9.7 Android版],中間是一個 WebView,上方是標題欄和工具欄

瀏覽器本身並不能直接改變屏幕的像素輸出,它需要通過系統本身的 GUI Toolkit。所以,一般來說瀏覽器會將一個要顯示的網頁包裝成一個 UI 組件,通常叫做 WebView,然後通過將 WebView 放置於應用的 UI 界面上,從而將網頁顯示在屏幕上。

一些 GUI Toolkit,比如 Android,默認的情況下 UI 組件沒有自己獨立的位圖緩存,構成 UI 界面的所有 UI 組件都直接繪製在當前的窗口緩存上,所以 WebView 每次繪製,就相當於將它在可見區域內的 RenderLayer/RenderObject 逐個繪製到窗口緩存上。上述的渲染方式有一個很嚴重的問題,用戶拖動網頁或者觸發一個慣性滾動時,網頁滑動的渲染性能會十分糟糕。這是因爲即使網頁只移動一個像素,整個 WebView 都需要重新繪製,而要繪製一個 WebView 大小的區域的 RenderLayer/RenderObject,耗時通常都比較長,對於一些複雜的桌面版網頁,在移動設備上繪製一次的耗時有可能需要上百毫秒,而要達到60幀/每秒的流暢度,每一幀繪製的時間就不能超過16.7毫秒,所以在這種渲染模式下,要獲得流暢的網頁滑屏效果,顯然是不可能的,而網頁滑屏的流暢程度,又是用戶對瀏覽器渲染性能的最直觀和最重要的感受。

要提升網頁滑屏的性能,一個簡單的做法就是讓 WebView 本身持有一塊獨立的緩存,而 WebView 的繪製就分成了兩步 1) 根據需要更新內部緩存,將網頁內容繪製到內部緩存裏面 2) 將內部緩存拷貝到窗口緩存上。第一步我們通常稱爲繪製(Paint)或者光柵化(Rasterization),它將一些繪圖指令轉換成真正的像素顏色值,而第二步我們一般稱爲混合(Composite),它負責緩存的拷貝,同時還可能包括位移(Translation),縮放(Scale),旋轉(Rotation),Alpha 混合等操作。咋一看,渲染變得比原來更復雜,還多了一步操作,但實際上,混合的耗時通常遠遠小於網頁內容繪製的耗時,後者即使在移動設備上一般也就在幾個毫秒以內,而大部分時候,在第一步裏面,我們只需要繪製一塊很小的區域而不需要繪製一個完整 WebView 大小的區域,這樣就有效地減少了繪製這一步的開銷。以網頁滾動爲例子,每次滾動實際上只需要繪製新進入 WebView 可見區域的部分,如果向上滾動了10個像素,我們需要繪製的區域大小就是10 x Width of WebView,比起原來需要繪製整個 WebView 大小區域的網頁內容當然要快的多了。

進一步來說,瀏覽器還可以使用多線程的渲染架構,將網頁內容繪製到緩存的操作放到另外一個獨立的線程(繪製線程),而原來線程對 WebView 的繪製就只剩下緩存的拷貝(混合線程),繪製線程跟混合線程之間可以使用同步,部分同步,完全異步等作業模式,讓瀏覽器可以在性能與效果之間根據需要進行選擇,比如說異步模式下,當瀏覽器需要將 WebView 緩存拷貝到窗口緩存,但是需要更新的部分還沒有來得及繪製時,瀏覽器可以在還未及時更新的部分繪製一個背景色或者空白,這樣雖然渲染效果有所下降,但是保證了每一幀窗口更新的間隔都在理想的範圍內。並且瀏覽器還可以爲 WebView 創建一個更大的緩存,超過 WebView本身的大小,讓我們可以緩存更多的網頁內容,可以預先繪製不可見的區域,這樣就可以有效減少異步模式下出現空白的狀況,在性能和效果之間取得更好的平衡。

硬件加速

上述的渲染模式,無論是繪製還是混合,都是由 CPU 完成的,而沒有使用到 GPU。繪製任務比較複雜,較難使用 GPU 來完成,並且對於各種複雜的圖形/文本的繪製來說,使用 GPU 效率有時反而更低(並且系統資源的開銷也較大),但是混合就不一樣了,GPU 最擅長的就是並行處理多個像素的計算,所以 GPU 相對於 CPU,執行混合的速度要快的多,特別是存在縮放,旋轉,Alpha 混合的時候,而且混合相對來說也比較簡單,改成使用 GPU 來完成並不困難。

並且在多線程渲染模式下,因爲繪製和混合分別處於不同的線程,繪製使用 CPU,混合使用 GPU,這樣可以通過 CPU/GPU 之間的併發運行有效地提升瀏覽器整體的渲染性能。更何況,窗口的更新是由混合線程來負責的,混合的效率越高,窗口更新的間隔就越短,用戶感受到 UI 界面變化的流暢度就越高,只要窗口更新的間隔能夠始終保持在16.7毫秒以內,UI 界面就能夠一直保持60幀/每秒的極致流暢度(因爲一般來說,顯示屏幕的刷新頻率是60hz,所以60幀/秒已經是極限幀率,超過這個數值意義不大,而且 OS 的圖形子系統本身就會強制限制 UI 界面的更新跟屏幕的刷新保持同步)。

所以對於現代瀏覽器來說,所謂硬件加速,就是使用 GPU 來進行混合,繪製仍然使用 CPU 來完成。

分塊渲染

Tile Rendering

圖片來自 [UC 瀏覽器 9.7 Android版],使用256×256大小的分塊

網頁的緩存通常都不是一大塊,而是劃分成一格一格的小塊,通常爲256×256或者512×512大小,這種渲染方式稱爲分塊渲染(Tile Rendering)。使用分塊渲染的主要原因是因爲 –

  1. 所謂 GPU 混合,通常是使用 Open GL/ES 貼圖來實現的,而這時的緩存其實就是紋理(GL Texture),而很多 GPU 對紋理的大小有限制,比如長/寬必須是2的冪次方,最大不能超過2048或者4096等,所以無法支持任意大小的緩存;
  2. 使用小塊緩存,方便瀏覽器使用一個統一的緩存池來管理分配的緩存,這個緩存池一般會分配成百上千個緩存塊供所有的 WebView 共用。所有打開的網頁,需要緩存時都可以以緩存塊爲單位向緩存池申請,而當網頁關閉或者不可見時,這些不需要的緩存塊就可以被回收供其它網頁使用;

總之固定大小的小塊緩存,通過一個統一緩存池來管理的方式,比起每個 WebView 自己持有一大塊緩存有很多優勢。特別是更適合多線程 CPU/GPU 併發的渲染模型,所以基本上支持硬件加速的瀏覽器都會使用分塊渲染的方式。

圖層混合加速

Layer Accelerated Compositing

圖片來自 [UC 瀏覽器 9.7 Android版],可見區域內有4個 Layer 有自己的緩存 – 最底層的 Base Layer,上方的 Fixed 標題欄,中間的熱點新聞欄,右下方的 Fixed 跳轉按鈕

圖層混合加速(Accelerated Compositing)的渲染架構是 Apple 引入 WebKit 的,並在 Safari 上率先實現,而 Chrome/Android/Qt/GTK+ 等都陸續完成了自己的實現。如果熟悉 iOS 或者 Mac OS GUI 編程的讀者對其應該不會感到陌生,它跟 iOS CoreAnimation 的 Layer Rendering 渲染架構基本類似,主要都是爲了解決當 Layer 的內容頻繁發生變化,或者當 Layer 觸發一個2D/3D變換(2D/3D Transform )或者漸隱漸入動畫,它的位移,縮放,旋轉,透明度等屬性不斷髮生變化時,在原有的渲染架構下,渲染性能低下的問題。

非混合加速的渲染架構,所有的 RenderLayer 都沒有自己獨立的緩存,它們都被繪製到同一個緩存裏面(按照它們的先後順序),所以只要這個 Layer 的內容發生變化,或者它的一些 CSS 樣式屬性比如 Transform/Opacity 發生變化,變化區域的緩存就需要重新生成,此時不但需要繪製變化的 Layer,跟變化區域(Damage Region)相交的其它 Layer 都需要被繪製,而前面已經說過,網頁的繪製是十分耗時的。如果 Layer 偶爾發生變化,那還不要緊,但如果是一個 JavaScript 或者 CSS 動畫在不斷地驅使 Layer 發生變化,這個動畫要達到60幀/每秒的流暢效果就基本不可能了。

而在混合加速的渲染架構下,一些 RenderLayer 會擁有自己獨立的緩存,它們被稱爲混合圖層(Compositing Layer),WebKit 會爲這些 RenderLayer 創建對應的 GraphicsLayer,不同的瀏覽器需要提供自己的 GrphicsLayer 實現用於管理緩存的分配,釋放,更新等等。擁有 GrphicsLayer 的 RenderLayer 會被繪製到自己的緩存裏面,而沒有 GrphicsLayer 的 RenderLayer 它們會向上追溯有 GrphicsLayer 的父/祖先 RenderLayer,直到 Root RenderLayer 爲止,然後繪製在有 GrphicsLayer 的父/祖先 RenderLayer 的緩存上,而 Root RenderLayer 總是會創建一個 GrphicsLayer 並擁有自己獨立的緩存。最終,GraphicsLayer 又構成了一棵與 RenderLayer 並行的樹,而 RenderLayer 與 GraphicsLayer 的關係有些類似於 RenderObject 與 RenderLayer 之間的關係。

混合加速渲染架構下的網頁混合,也變得比以前複雜,不再是簡單的將一個緩存拷貝到窗口緩存上,而是需要完成源自不同 Layer 的多個緩存的拷貝,再加上可能的2D/3D變換,再加上緩存之間的Alpha混合等操作,當然,對於支持硬件加速,使用 GPU 來完成混合的瀏覽器來說,速度還是很快的。

RenderLayer 生成 GraphicsLayer 的條件,來自 GPU Accelerated Compositing in Chrome

  1. Layer has 3D or perspective transform CSS properties
  2. Layer is used by < video> element using accelerated video decoding
  3. Layer is used by a < canvas> element with a 3D context or accelerated 2D context
  4. Layer is used for a composited plugin
  5. Layer uses a CSS animation for its opacity or uses an animated webkit transform
  6. Layer uses accelerated CSS filters
  7. Layer with a composited descendant has information that needs to be in the composited layer tree, such as a clip or reflection
  8. 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)

混合加速的渲染架構下,Layer 的內容變化,只需要更新所屬的 GraphicsLayer 的緩存即可,而緩存的更新,也只需要繪製直接或者間接屬於這個 GraphicsLayer 的 RenderLayer 而不是所有的 RenderLayer。特別是一些特定的 CSS 樣式屬性的變化,實際上並不引起內容的變化,只需要改變一些 GraphicsLayer 的混合參數,然後重新混合即可,而混合相對繪製而言是很快的,這些特定的 CSS 樣式屬性我們一般稱之爲是被加速的,不同的瀏覽器支持的狀況不太一樣,但基本上 CSS Transform & Opacity 在所有支持混合加速的瀏覽器上都是被加速的。被加速的CSS 樣式屬性的動畫,就比較容易達到60幀/每秒的流暢效果了。

Falling Leaves

圖片來自 Understanding Hardware Acceleration on Mobile Browsers ,展現了經典的 CSS 動畫 Demo – Falling Leaves 的圖層混合的示意圖,它所使用的 Transform 和 Opacity 動畫在所有支持混合加速的瀏覽器上都是被加速的

不過並不是擁有獨立緩存的 RenderLayer 越多越好,太多擁有獨立緩存的 Layer 會帶來一些嚴重的副作用 – 首先它大大增加了內存的開銷,這點在移動設備上的影響更大,甚至導致瀏覽器在一些內存較少的移動設備上無法很好地支持圖層混合加速;其次,它加大了混合的時間開銷,導致混合性能的下降,而混合性能跟網頁滾動/縮放操作的流暢度又息息相關,最終導致網頁滾動/縮放的流暢度下降,讓用戶覺得操作不夠流暢。

在 Chrome://flags 裏面開啓“合成渲染層邊框”就可以看到哪些 Layer 是一個 Compositing Layer,也就是擁有自己的獨立緩存。前端開發者可以用此幫助自己控制 Compositing Layer 的創建。 總的的說, Compositing Layer 可以提升繪製性能,但是會降低混合性能,網頁只有合理地使用 Compositing Layer,才能在繪製和混合之間取得一個良好的平衡,實現整體渲染性能的提升。

Chrome Flags

圖片來自 Chrome,展現了經典的 CSS 動畫 Demo – Falling Leaves 的合成渲染層邊框

網頁遊戲渲染 - Canvas & WebGL

2D Canvas

圖片來自 [UC 瀏覽器 9.7 Android版],基於2D Canvas 的遊戲不江湖,在主流配置手機上可以達到60幀/每秒的流暢度

以前網頁遊戲一般都是使用 Flash 來實現,但是隨着 Flash 從移動設備被淘汰,越來越多的網頁遊戲會改用 Canvas 和 WebGL 來開發,瀏覽器關於 Canvas 的基本繪製流程可以參考我以前的文章 Introduce My Work 。雖然一般網頁元素都是使用 CPU 來繪製,但是對於加速的2D Canvas 和 WebGL 來說,它們的繪製是直接使用 GPU 的,所以它們一般會擁有一個 GL FBO (FrameBufferObject)作爲自己的緩存,Canvas/WebGL 的內容被繪製到這個 FBO 上面,而這個 FBO 所關聯的紋理再在混合操作裏面被拷貝到窗口緩存上。簡單的來說,對於加速的2D Canvas 和 WebGL,它們的繪製和混合都是使用 GPU。

WebGL

圖片來自 [UC 瀏覽器 9.7 Android版],一個演示 WebGL 的網頁 Demo

關於如何優化 Canvas 遊戲的性能,請參考我以前的文章 – High Performance Canvas Game for Android(高性能Android Canvas遊戲開發)

參考索引

How Browsers Work: Behind the scenes of modern web browsers

How WebKit Work

WebKit for Developers

GPU Accelerated Compositing in Chrome

Understanding Hardware Acceleration on Mobile Browsers

Web Page Rendering and Accelerated Compositing

我的2013 – 年終總結 + 瀏覽器渲染髮展的一些思考

Introduce My Work

High Performance Canvas Game for Android(高性能Android Canvas遊戲開發)

OpenGL Frame Buffer Object (FBO)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章