梳理瀏覽器渲染流程
首先簡單瞭解一下瀏覽器請求、載入、渲染一個頁面的大致過程:
- DNS 查詢
- TCP 連線
- HTTP 請求即響應
- 伺服器響應
- 客戶端渲染
這裡主要將客戶端渲染展開梳理一下,從瀏覽器器核心拿到內容(渲染執行緒接收請求,載入網頁並渲染網頁),渲染大概可以劃分成以下幾個步驟:
- 解析html建立dom樹
- 解析css構建render樹(將CSS程式碼解析成樹形的資料結構,然後結合DOM合併成render樹)
- 佈局render樹(Layout/reflow),負責各元素尺寸、位置的計算
- 繪製render樹(paint),繪製頁面畫素資訊
- 瀏覽器會將各層的資訊傳送給GPU(GPU程式:最多一個,用於3D繪製等),GPU會將各層合成(composite),顯示在螢幕上。
參考一張圖(webkit渲染主要流程):
這裡先解釋一下幾個概念,方便大家理解:
DOM Tree:瀏覽器將HTML解析成樹形的資料結構。
CSS Rule Tree:瀏覽器將CSS解析成樹形的資料結構。
Render Tree: DOM和CSSOM合併後生成Render Tree。
layout: 有了Render Tree,瀏覽器已經能知道網頁中有哪些節點、各個節點的CSS定義以及他們的從屬關係,從而去計算出每個節點在螢幕中的位置。
painting: 按照算出來的規則,通過顯示卡,把內容畫到螢幕上。
reflow(迴流):當瀏覽器發現某個部分發生了點變化影響了佈局,需要倒回去重新渲染,內行稱這個回退的過程叫 reflow。reflow 會從 <html> 這個 root frame 開始遞迴往下,依次計算所有的結點幾何尺寸和位置。reflow 幾乎是無法避免的。現在介面上流行的一些效果,比如樹狀目錄的摺疊、展開(實質上是元素的顯 示與隱藏)等,都將引起瀏覽器的 reflow。滑鼠滑過、點選……只要這些行為引起了頁面上某些元素的佔位面積、定位方式、邊距等屬性的變化,都會引起它內部、周圍甚至整個頁面的重新渲 染。通常我們都無法預估瀏覽器到底會 reflow 哪一部分的程式碼,它們都彼此相互影響著。
repaint(重繪):改變某個元素的背景色、文字顏色、邊框顏色等等不影響它周圍或內部佈局的屬性時,螢幕的一部分要重畫,但是元素的幾何尺寸沒有變。
注意:
- display:none 的節點不會被加入Render Tree,而visibility: hidden
則會,所以,如果某個節點最開始是不顯示的,設為display:none是更優的。 - display:none 會觸發 reflow,而 visibility:hidden 只會觸發 repaint,因為沒有發現位置變化。
- 有些情況下,比如修改了元素的樣式,瀏覽器並不會立刻reflow 或 repaint 一次,而是會把這樣的操作積攢一批,然後做一次reflow,這又叫非同步 reflow 或增量非同步 reflow。但是在有些情況下,比如resize視窗,改變了頁面預設的字型等。對於這些操作,瀏覽器會馬上進行 reflow。
再參考一張圖理解一下:
細緻分離兩個環節,其他環節參考上述概念註解:
JavaScript
:JavaScript實現動畫效果,DOM元素操作等。Composite(渲染層合併)
:對頁面中 DOM 元素的繪製是在多個層上進行的。在每個層上完成繪製過程之後,瀏覽器會將所有層按照合理的順序合併成一個圖層,然後顯示在螢幕上。對於有位置重疊的元素的頁面,這個過程尤其重要,因為一旦圖層的合併順序出錯,將會導致元素顯示異常。
在實際場景下,大致會出現三種常見的渲染流程(Layout和Paint步驟是可避免的,可參考上一張圖的注意部分理解):
Composite
瞭解層
注意:首先說明,這裡討論的是 WebKit,描述的是 Chrome 的實現細節,而並非是 web 平臺的功能,因此這裡介紹的內容不一定適用於其他瀏覽器。
- Chrome 擁有兩套不同的渲染路徑(rendering path):硬體加速路徑和舊軟體路徑(older software path)
- Chrome 中有不同型別的層: RenderLayer(負責 DOM 子樹)和GraphicsLayer(負責 RenderLayer的子樹),只有 GraphicsLayer 是作為紋理(texture)上傳給GPU的。
- 什麼是紋理?可以把它想象成一個從主儲存器(例如 RAM)移動到影象儲存器(例如 GPU 中的 VRAM)的點陣圖影象(bitmapimage)
- Chrome 使用紋理來從 GPU上獲得大塊的頁面內容。通過將紋理應用到一個非常簡單的矩形網格就能很容易匹配不同的位置(position)和變形(transformation)。這也就是3DCSS 的工作原理,它對於快速滾動也十分有效。
整個圖:
在 Chrome 中其實有幾種不同的層型別:
- RenderLayers 渲染層,這是負責對應 DOM 子樹
- GraphicsLayers 圖形層,這是負責對應 RenderLayers子樹。
在瀏覽器渲染流程中提到了composite概念,在 DOM 樹中每個節點都會對應一個 LayoutObject,當他們的 LayoutObject 處於相同的座標空間時,就會形成一個 RenderLayers ,也就是渲染層。RenderLayers 來保證頁面元素以正確的順序合成,這時候就會出現層合成(composite),從而正確處理透明元素和重疊元素的顯示。
某些特殊的渲染層會被認為是合成層(Compositing Layers),合成層擁有單獨的 GraphicsLayer,而其他不是合成層的渲染層,則和其第一個擁有 GraphicsLayer 父層公用一個。
而每個GraphicsLayer(合成層單獨擁有的圖層) 都有一個 GraphicsContext,GraphicsContext 會負責輸出該層的點陣圖,點陣圖是儲存在共享記憶體中,作為紋理上傳到 GPU 中,最後由 GPU 將多個點陣圖進行合成,然後顯示到螢幕上。
如何變成合成層
合成層建立標準
什麼情況下能使元素獲得自己的層?雖然 Chrome的啟發式方法(heuristic)隨著時間在不斷髮展進步,但是從目前來說,滿足以下任意情況便會建立層:
- 3D 或透視變換(perspective transform) CSS 屬性
- 使用加速視訊解碼的 <video> 元素 擁有 3D
- (WebGL) 上下文或加速的 2D 上下文的 <canvas> 元素
- 混合外掛(如 Flash)
- 對自己的 opacity 做 CSS動畫或使用一個動畫變換的元素
- 擁有加速 CSS 過濾器的元素
- 元素有一個包含複合層的後代節點(換句話說,就是一個元素擁有一個子元素,該子元素在自己的層裡)
- 元素有一個z-index較低且包含一個複合層的兄弟元素(換句話說就是該元素在複合層上面渲染)
合成層的優點
淘寶的栗子舉的很詳細,值得一看,裡面提到了一旦renderLayer提升為了合成層就會有自己的繪圖上下文,並且會開啟硬體加速,有利於效能提升,裡面列舉了一些特點
- 合成層的點陣圖,會交由 GPU 合成,比 CPU 處理要快
- 當需要 repaint 時,只需要 repaint 本身,不會影響到其他的層
- 對於 transform 和 opacity 效果,不會觸發 layout 和 paint
注意:
- 提升到合成層後合成層的點陣圖會交GPU處理,但請注意,僅僅只是合成的處理(把繪圖上下文的點陣圖輸出進行組合)需要用到GPU,生成合成層的點陣圖處理(繪圖上下文的工作)是需要CPU。
- 當需要repaint的時候可以只repaint本身,不影響其他層,但是paint之前還有style, layout,那就意味著即使合成層只是repaint了自己,但style和layout本身就很佔用時間。
- 僅僅是transform和opacity不會引發layout 和paint,那麼其他的屬性不確定。
總結合成層的優勢:一般一個元素開啟硬體加速後會變成合成層,可以獨立於普通文件流中,改動後可以避免整個頁面重繪,提升效能。
效能優化點:
- 提升動畫效果的元素 合成層的好處是不會影響到其他元素的繪製,因此,為了減少動畫元素對其他元素的影響,從而減少paint,我們需要把動畫效果中的元素提升為合成層。
提升合成層的最好方式是使用 CSS 的 will-change屬性。從上一節合成層產生原因中,可以知道 will-change 設定為opacity、transform、top、left、bottom、right 可以將元素提升為合成層。
- 使用 transform 或者 opacity 來實現動畫效果, 這樣只需要做合成層的合併就好了。
- 減少繪製區域 對於不需要重新繪製的區域應儘量避免繪製,以減少繪製區域,比如一個 fix 在頁面頂部的固定不變的導航header,在頁面內容某個區域 repaint 時,整個螢幕包括 fix 的 header 也會被重繪。
而對於固定不變的區域,我們期望其並不會被重繪,因此可以通過之前的方法,將其提升為獨立的合成層。減少繪製區域,需要仔細分析頁面,區分繪製區域,減少重繪區域甚至避免重繪。
利用合成層可能踩到的坑
- 合成層佔用記憶體的問題
- 層爆炸,由於某些原因可能導致產生大量不在預期內的合成層,雖然有瀏覽器的層壓縮機制,但是也有很多無法進行壓縮的情況,這就可能出現層爆炸的現象(簡單理解就是,很多不需要提升為合成層的元素因為某些不當操作成為了合成層)。解決層爆炸的問題,最佳方案是打破 overlap 的條件,也就是說讓其他元素不要和合成層元素重疊。簡單直接的方式:
使用3D硬體加速提升動畫效能時,最好給元素增加一個z-index屬性,人為干擾合成的排序,可以有效減少chrome建立不必要的合成層,提升渲染效能,移動端優化效果尤為明顯。
在這篇文章中的demo可以看出其中厲害。
用chremo開啟demo頁面後,開啟瀏覽器的開發者模式,再按照如圖操作開啟檢視工具:
開啟 Rendering 的Layer borders後 觀察點選為動畫元素設定z-index核取方塊
的頁面提示變化:
上圖中可以明顯看出:頁面中設定了一個h1標題,應用了translate3d動畫,使得它被放到composited layer中渲染,然後在這個元素後面建立了2000個list。在不為h1元素設定z-index的情況下,使得本不需要提升到合成層的ul元素下的每個li元素都提升為一個單獨合成層(每個li元素的黃色提示邊框),最終會導致GPU資源過度消耗頁面滑動時很卡,尤其在移動端(安卓)上更加明顯。
如上圖操作選中為動畫元素設定z-index
,可以看出ul下的每個li都回歸到普通渲染層,不再是合成層也就不會消耗GPU資源去渲染,從而達到了優化頁面效能優化的目的。
大家可以用支援『硬體加速』的『安卓』手機瀏覽器測試上述頁面,給動畫元素加z-index前後的效能差距非常明顯。
最後
在實際的前端開發中尤其是移動端開發,很多小夥伴都很喜歡使用類似 translateZ(0)等屬性來進行所謂的硬體加速,以提升效能,達到優化頁面動態效果的目的,但還是要注意凡事過猶不及,應用硬體加速的同時也要注意到千萬別踩坑。
關於合成層的更細緻具體的講解,可以仔細學習下下面的參考文章(尤其是前三篇哦)。
最後祝願熱愛技術的你我始終堅持在探索技術的路上奮力前行!
參考文章:
無線效能優化:Composite
DOM to Screen
CSS GPU Animation: Doing It Right
web優化之composite
詳談層合成(composite)
CSS3硬體加速也有坑