singsong: 文本是本身看了一些不錯資料整理出來的,對該知識點感興趣的同窗能夠查看 參考文章小節。
主要爲 CSS 優化工做打一下基礎。要編寫高性能的網站和應用,除了確保編寫的代碼儘量高效地運行外,還須要確保頁面性能,刷新頻率儘可能到達 60FPS。這就須要瞭解瀏覽器是如何進行渲染的。而瀏覽器渲染與 CSS 密切相關,所以只有瞭解其中工做原理才能讓 CSS 更好地工做。html
另外,接下來會出一篇優化實戰文章,會涉及 JavaScript 和 CSS 一些優化。其中關於 JavaScript 的優化以前已進行過介紹:常見的 JavaScript 內存泄露。本文是對 CSS 優化進行一個補充。html5
本文主要介紹瀏覽器的渲染,即渲染引擎(Rendering engine)負責的工做: 將請求的 HTML 和 CSS 內容解析並渲染在屏幕上。git
DOM:Document Object Model,文檔對象模型。它能夠以一種獨立於平臺和語言的方式,訪問和修改一個文檔的內容和結構。它定義了一組與平臺,語言無關的接口,該接口容許編程語言動態地訪問和修改結構化文檔。基於 DOM 表示的文檔被描述成一個樹形結構,使用 DOM 的接口能夠對 DOM 樹進行操做。github
這裏以一個實例展開講解,以下 HTML 結構包含一些文本和一張圖片:web
<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 頁面:chrome
<html>
、<body>
,以及其餘尖括號內的字符串。每一個 token 都具備必定特殊含義和規則。整個過程的最終輸出是 HTML 頁面的 DOM tree,後續瀏覽器對頁面進一步的全部處理都會用到它。瀏覽器每次處理 HTML 標籤時,都會完成以上全部步驟:將字節轉換成字符,肯定 token,將 token 轉換成節點,而後構建 DOM tree。編程
CSSOM:CSS Object Model,CSS 對象模型。CSSOM 定義了 JavaScript 訪問樣式的能力和方式。它是在 DOM 中的一些接口中,加入獲取和操做 CSS 屬性或接口的 JavaScript 接口,於是 JavaScript 能夠動態操做 CSS 樣式。DOM 提供了接口讓 JavaScript 修改 HTML 文檔,CSSOM 提供了接口讓 JavaScript 得到和修改 CSS 代碼設置的樣式信息。canvas
在瀏覽器構建 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 能夠用於肯定節點對象的計算樣式。如 span 標記包含了color:red
樣式和繼承於 body 標記的font-size:16px
樣式;
meta
、head
、script
等,以及經過 CSS 樣式display:none
隱藏的節點。相反可見節點是用戶可見的,如body
、div
、span
、canvas
、img
等。對於這些可見節點,瀏覽器須要將它們的內容繪製到最終的頁面中,因此瀏覽器會爲它們創建對應的 RenderObject 對象。一個 RenderObject 對象保存了爲繪製 DOM 節點的各類信息。這些 RenderObject 對象與 DOM 對象相似,也構成一棵樹,稱爲RenderObject tree。RenderObject tree 是基於 DOM tree 創建起來的一棵新樹,是爲了佈局計算和渲染等機制而構建的一種新的內部表示。RenderObject tree 節點與 DOM tree 節點不是一一對應關係。由於建立一個 RenderObject 對象須要知足以下規則:html
、body
、div
等。而瀏覽器不會爲不可見節點建立 RenderObject 節點。RenderObject 對象構成了 RenderObject tree,每一個 RenderObject 對象保存了爲繪製 DOM 節點的計算樣式。RenderObject tree 也能夠理解成由 CSSOM tree 和 DOM tree 合併成:
當瀏覽器建立 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%。
瀏覽器渲染引擎並非直接使用 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:
<canvas>
element that has a 3D (WebGL) context or an accelerated 2D context<video>
element翻譯:
每一個 RenderLayer 對象能夠想象成圖像中一個圖層,各個圖層疊加構成了一個圖像。瀏覽器會遍歷 RenderLayer tree,再遍歷從屬這個 RenderLayer 的 RenderObject,RenderObject 對象存儲有繪製信息,並進行繪製。RenderLayer 和 RenderObject 共同決定了最終呈現的網頁內容,RenderLayer tree 決定了網頁的繪製的層次順序,而從屬於 RenderLayer 的 RenderObject 決定了該 RenderLayer 的內容。
在完成構建 RenderLayer tree 以後,瀏覽器會使用圖形庫將其構建的渲染模型繪製出來,該過程分爲兩個階段:
繪製:將從屬每一個 RenderLayer 圖層上的 RenderObject 繪製在其 RenderLayer 上。即繪製(Paint)或者光柵化(Rasterization),將一些繪圖指令轉換成真正的像素顏色值。
渲染引擎的渲染,目前有三種網頁的渲染方式:
- 硬件加速合成(Accelerated Compositing):使用 GPU 來完成合成工做。
- 合成化渲染:使用合成(compositing)技術的渲染稱。
第二種和第三種渲染方式,都是使用了合成化渲染技術,合成工做也都是由 GPU 來作。對於常見的 2D 繪圖操做,使用 GPU 來繪圖不必定比使用 CPU 繪圖在性能上有優點,例如繪製文字、點、線等。緣由是 CPU 的使用緩衝機制有效減小了重複繪製的開銷並且不須要考慮與 GPU 並行。另外,GPU 的內存資源相對 CPU 的內存資源來講比較緊張,並且網頁的分層使得 GPU 的內存使用相對比較多。鑑於此,就目前的狀況來看,三者都存在是有其合理性的,下面分析一下它們的特色:
瀏覽器還可使用多線程的渲染架構,將網頁內容繪製到後端存儲的操做放到另一個獨立的線程(繪製線程),而原來線程轉爲合成線程,繪製線程跟合成線程之間可使用同步,部分同步,徹底異步等做業模式,讓瀏覽器能夠在性能與效果之間根據須要進行選擇。
對於軟件渲染而言,到 RenderLayer tree 就結束了,後面不會創建其它額外的樹來對應於 RenderLayer tree。可是,對於硬件渲染來講,在 RenderLayer tree 以後,瀏覽器渲染引擎爲硬件渲染提供了更多的內部結構來支持這個機制。
在硬件加速渲染的合成化渲染和軟件繪圖的合成化渲染架構下,一個 RenderLayer 對象若是須要後端存儲,它會建立一個 RenderLayerBacking 對象,該對象負責 Renderlayer 對象所須要的各類存儲。理想狀況下,每一個 RenderLayer 均可以建立本身的後端存儲,事實上不是全部 RenderLayer 都有本身的 RenderLayerBacking 對象。若是一個 RenderLayer 對象被像樣的建立後端存儲,那麼將該 RenderLayer 稱爲合成層(Compositing Layer)。
哪些 RenderLayer 對象能夠是合成層?若是一個 RenderLayer 對象具備如下的特徵之一,那麼它就是合成層:
翻譯:
<video>
元素<canvas>
元素opacity
或transform
動畫每一個合成層都有一個 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 tree、RenderObject tree、RenderLayer tree、GraphicsLayer tree關係圖:
這樣能夠合併一些 RenderLayer 層,從而減小內存的消耗。其次,合併以後,減小了合併帶來的重繪性能和處理上的困難。在硬件加速渲染的合成化渲染和軟件繪圖的合成化渲染架構下,RenderLayer 的內容變化,只須要更新所屬的 GraphicsLayer 的緩存便可,而緩存的更新,也只須要繪製直接或者間接屬於這個 GraphicsLayer 的 RenderLayer,而不是全部的 RenderLayer。特別是一些特定的 CSS 樣式屬性的變化,實際上並不引發內容的變化,只須要改變一些 GraphicsLayer 的混合參數,而後從新混合便可,而混合相對繪製而言是很快的,這些特定的 CSS 樣式屬性咱們通常稱之爲是被加速的,不一樣的瀏覽器支持的情況不太同樣,但基本上 CSS Transform & Opacity 在全部支持混合加速的瀏覽器上都是被加速的。被加速的 CSS 樣式屬性的動畫,就比較容易達到 60 幀/每秒的流暢效果了。
不過並非擁有獨立緩存的 RenderLayer 越多越好,太多擁有獨立緩存的 RenderLayer 會帶來一些嚴重的反作用:
一般一個合成層的後端存儲被分割成多個大小相同的瓦片狀的小存儲空間,每一個瓦片能夠理解爲 OpenGL 中的一個紋理,合成層的結果被分開存儲在這些瓦片中。爲何使用瓦片化的後端存儲?
網頁加載後,繪製新的每一幀,通常都須要通過計算佈局(layout)、繪圖(paint)、合成(composite)三階段。所以要想提升頁面性能(或 FPS),須要減小每一幀的時間。而在這三個階段中,layout 和 paint 比較耗時間,而合成須要的時間相對較少一些。
若是修改 DOM 元素的 layout 樣式(如 width, heihgt 等),瀏覽器會計算頁面須要 relayout 的元素,而後觸發一個 relayout。被 relayout 的元素,接着會執行繪製,最後進行渲染合併生成頁面。
若是修改 DOM 元素的 paint 樣式(如 color, background 等),瀏覽器會跳過佈局,直接執行繪製,再進行合成。
若是修改 DOM 元素的 composite 樣式(如 transform, opacity 等)。瀏覽器會跳過佈局和繪製,直接執行合成。該過程是開銷最小的,也是優化着手點。
若是想知道修改任何指定 CSS 樣式會觸發 layout、paint、composite 中的哪個,請查看CSS 觸發器。
能夠經過什麼途徑進行優化,減小每一幀的時間(避免過多 layout 或 paint):
即便用 GPU 硬件加速,爲某些 RenderLayer 建立對應 GraphicsLayer。經過爲每個合成層設置transform
屬性來完成 transition 或 animation,有效地避免 relayout 和 repaint 的開銷。
本文重點介紹了瀏覽器渲染引擎的渲染過程,涉及了 DOM tree、CSSOM tree、RenderObject tree、RenderLayer tree、GraphicsLayer tree。並對各類渲染模式進行了簡單介紹,其中引入了硬件加速機制,還給出一些優化建議。瞭解這些知識點對咱們開發高性能的 web 應用會有很大的幫助。