若干年前,我寫過一篇介紹瀏覽器渲染流水線的文章 - How Rendering Work (in WebKit and Blink),這篇文章,一來部份內容已通過時,二來缺乏一個全局視角來對流水線總體進行分析,因此打算從新寫一篇新的文章,從一個更高抽象層次和高度簡化的方式對瀏覽器的渲染流水線進行解析,能讓大部分頁端同窗都可以看的明白,並以此做爲指引來分析和優化頁面的渲染/動畫性能。瀏覽器
有些基本概念如圖層,分塊,光柵化基本沒有發生變化,若是讀者不理解的話請參考 How Rendering Work (in WebKit and Blink),本文再也不過多解釋。緩存
本文基於當前版本的 Chrome 瀏覽器寫成(60 左右),理論上部分知識能夠應用於其它瀏覽器(固然術語會有必定差異)或者 Chrome 後續的版本,可是並不徹底保證這一點。性能優化
上圖顯示了 Chrome 一個高度簡化後的渲染流水線示意圖:網絡
當咱們說 Compositor,在沒有加修飾語的狀況下,通常都是指 Layer Compositor。另外術語 Child Compositor(子合成器)也是指 Layer Compositor,相對於做爲 Parent 的 Display Compositor 而言。多線程
一個 Chrome 瀏覽器通常會有一個 Browser 進程,一個 GPU 進程,和多個 Renderer 進程,一般每一個 Renderer 進程對應一個頁面。在特殊架構(Android WebView)或者特定配置下,Browser 進程能夠兼做 GPU 進程或者 Renderer 進程(意味着沒有獨立的 GPU 或者 Renderer 進程),可是 Browser 跟 Renderer,Browser 跟 GPU,Renderer 跟 GPU 之間的系統架構和通信方式基本保持不變,線程架構也是一樣。架構
Display Compositor 將來應該會移到 GPU 進程的主 GPU 線程,固然對父子合成器進行調度的部分仍然是在 Browser 進程的 UI 線程。併發
全部的渲染流水線都會有幀的概念,幀這個概念抽象描述了渲染流水線下級模塊往上級模塊輸出的繪製內容相關數據的封裝。咱們能夠看到 Blink 輸出 Main Frame 給 Layer Compositor,Layer Compositor 輸出 Compositor Frame 給 Display Compositor,Display Compositor 輸出 GL Frame 給 Window。咱們以爲一個動畫是否流暢,最終取決於 GL Frame 的幀率(也就是目標窗口的繪製更新頻率),而以爲一個觸屏操做是否響應即時,取決於從 Blink 處理事件到 Window 更新的整個過程的耗時(理論上應該還要加上事件從 Browser 發送給 Compositor,再發送給 Blink 的這個過程的耗時)。異步
Main Frame 包含了對網頁內容的描述,主要以繪圖指令的形式,或者能夠簡單理解爲某個時間點對整個網頁的一個矢量圖快照(能夠局部更新)。當前版本的 Chrome,圖層化的決策仍然由 Blink 來負責,Blink 須要決定如何根據網頁的 DOM 樹來生成一顆圖層樹,並以 DisplayList 的形式記錄每一個圖層的內容(將來圖層化決策應該會轉移到 Layer Compositor,Blink 只輸出 DisplayList 樹和 DisplayList 節點的關鍵屬性,同時 DisplayList 再也不以圖層做爲單位,而是以每一個排版對象做爲單位)。ide
圖層化決策通常由如下幾個因素決定:性能
第三點是能夠被頁端所直接控制來優化圖層結構及 Main Frame 性能,像傳統的 translate3d hack 和新的 CSS 屬性 will-change。
Layer Compositor 接收 Blink 生成的 Main Frame,並轉換成合成器內部的圖層樹結構(由於圖層化決策仍然由 Blink 負責,因此這裏的轉換基本上能夠認爲是生成一棵一樣的樹,再對逐個圖層的進行拷貝)。
Layer Compositor 須要爲每一個圖層進行分塊,爲每一個分塊分配 Resource(Texture 的封裝),而後安排光柵化任務。
當 Layer Compositor 接收到來自 Browser 的繪製請求時,它會爲當前可見區域的每一個圖層的每一個分塊生成一個 Draw Quad 的繪製指令(矩形繪製,指令實際上指定了座標,大小,變換矩陣等屬性),全部的 Draw Quad 指令和對應的 Resource 的集合就構成了 Compositor Frame。Compositor Frame 被髮送往 Browser,並最終到達 Display Compositor(將來也能夠直接發給 Display Compositor)。
Display Compositor 將 Compositor Frame 的每一個 Draw Quad 繪製指令轉換一個 GL 多邊形繪圖指令,使用對應 Resource 封裝的 Texture 對目標窗口進行貼圖,這些 GL 繪圖指令的集合就構成了一個 GL Frame,最終由 GPU 執行這些 GL 指令完成網頁在窗口上佔據的可見區域的繪製。
Chrome 渲染流水線的調度是基於請求和狀態機響應,調度的最上級中樞運行在 Browser UI 線程,它按顯示器的 VSync(垂直同步)週期向 Layer Compositor 發出輸出下一幀的請求,而 Layer Compositor 根據自身狀態機的狀態決定是否須要 Blink 輸出下一幀。
Display Compositor 則比較簡單,它持有一個 Compositor Frame 的隊列不斷的進行取出和繪製,輸出的頻率惟二地取決於 Compositor Frame 的輸入頻率和自身繪製 GL Frame 的耗時。基本上能夠認爲 Layer Compositor 和 Display Compositor 是生產者和消費者的關係。
動畫能夠看作是一個連續的幀序列的組合。咱們把網頁的動畫分紅兩大類 —— 一類是合成器動畫,一類是非合成器動畫(UC 內部也將其稱爲內核動畫,雖然這不是 Chrome 官方的術語)。
合成器動畫又能夠分爲兩類:
Blink 觸發的動畫,若是是 Transform 和 Opacity 屬性的動畫基本上均可以由合成器運行,由於它們沒有改變圖層的內容。不過即便能夠交由合成器運行,它們也須要產生一個新的 Main Frame 提交給合成器來觸發這個動畫,若是這個 Main Frame 包含了大量的圖層變動,也會致使觸發的瞬間卡頓,頁端事先對圖層結構進行優化能夠避免這個問題。
非合成器動畫也能夠分爲兩類:
合成器動畫和非合成器動畫在渲染流水線上有較大的差別,後者更復雜,流水線更長。上面四種動畫的分類,按渲染流水線的複雜程度和理論性能排列(複雜程度由低到高,理論性能由高到低):
長久以來,瀏覽器渲染流水線的設計都主要是爲了合成器動畫的性能而優化,甚至在某種程度上致使內核動畫性能的降低,好比說合成器的異步光柵化機制。不過這兩年,隨着對 WebApp 渲染性能包括 WebGL 性能的重視,而且隨着主流移動設備的硬件性能持續提高,合成器動畫的性能也已經基本不成問題,Chrome 的渲染流水線已經更多地針對內核動畫的性能進行優化,甚至會致使在某些特定情況下合成器動畫性能的降低,比方說傾向於爲了維持圖層樹的穩定性,減小變動,而生成更多的圖層。不過總的說來,目前 Chrome 的渲染流水線,在主流的移動設備上,大部分場景下,二者性能都能得到一個較好的平衡。
這裏的性能分析主要是針對移動設備,以桌面處理器的性能,大部分場景下都不存在性能問題。目前移動設備的屏幕刷新率基本上都是 60hz,而瀏覽器跟其它應用同樣,須要跟屏幕刷新保持垂直同步,也就是動畫幀率的上限是 60 幀,這也是咱們可以達到的最理想的結果。不過考慮瀏覽器自己的複雜程度,可能有不少後臺任務在運行,並且操做系統自己也可能同時運行其它後臺任務,而且移動平臺要考慮能耗和散熱,CPU/GPU 的調度策略會頻繁地發生變化,要徹底鎖定 60 幀是很是困難的。
若是上限超過 60 幀,實際平均幀率超過 60 反而不難,可是若是上限是 60 幀,垂直同步下要鎖定 60 幀是很是困難的,要求每一幀的各個環節耗時都要保持很是穩定。
通常而言:
要達到 50 幀以上的水平,咱們就須要對動畫在渲染流水線的每一個重要環節進行性能計算,須要知道這些環節最長容許的耗時上限和網頁影響這些環節耗時的主要緣由,雖然實際上很難徹底鎖定 60 幀,可是通常來講性能分析/優化仍是會以 60 幀爲目標來倒推各個環節的最大耗時。
若是是場景比較複雜的 Canvas/WebGL 遊戲,以 30 幀爲目標幀率是一個合理的訴求。
在對動畫性能進行分析以前,須要先說明一下目前的 Chrome 的光柵化機制。合成器會監控是否須要安排新的光柵化任務,當須要光柵化調度時:
實際的光柵化區域會比當前可見區域要更大一些,通常是增長一個分塊大小單位,對不可見區域的預光柵化有助於提高合成器動畫的性能和減小出現空白的概率。
從上可知,合成器的光柵化調度徹底是異步的,合成器在 Compositor 線程須要執行的就是安排光柵化任務和檢查哪些任務已經完成,Compositor 線程自己不會被真正運行光柵化任務的 Worker 線程所阻塞。
上圖顯示了合成器動畫的渲染流水線示意圖,根據 Android WebView 平臺的實現進行繪製,其它平臺可能略微不一樣,但對後面的性能分析,在大部分狀況下影響不大
整個流水線的大概過程是:
上述流程的一些關鍵點是:
總的來講影響合成器動畫性能的最關鍵因素就是過分繪製係數(Overdraw,能夠理解爲繪製的面積和可見區域面積的比例),若是網頁自己存在大量圖層堆疊狀況,致使過分繪製係數太高,就會嚴重影響合成器動畫的性能。經驗顯示,過分繪製係數比較理想的值是在 2 之內,通常建議不超過 3,這樣能夠保證在中低端的移動設備上也有不錯的性能表現。
另外,合成器動畫過程當中,Compositor 和 GPU 線程是前臺線程,它們雖然理論上不會被 Worker 和 Renderer 線程阻塞,可是在真實的運行場景中,移動設備的 CPU/GPU 和內存帶寬等硬件資源是有限的,若是 Worker 和 Renderer 線程處於高負荷狀態下,也會致使前臺的 Compositor 和 GPU 線程阻塞,最終致使合成器動畫掉幀。
這種現象常見於:
根據上述的耗時分析,咱們能夠給出一個頁端優化合成器動畫性能的簡單 Checklist:
如何判斷網頁的圖層結構是否穩定,通常而言,若是是位於葉子節點的圖層增長或者移除,對整個圖層結構影響並不大,可是若是是中間節點的圖層增長或者移除,對圖層結構的影響就比較大了,而且越是接近根節點,影響就越大。
如今的頁端都會大量使用異步加載來優化加載性能和流量,可是容易出現致使動畫掉幀的現象。要平衡好這一點意味着須要實現一個加載和關聯 DOM 操做的調度器,若是檢查到動畫正在運行,則中止加載或者經過節流閥機制下降加載的併發數量和頻率,同時能夠經過事先生成相應的 DOM 節點和圖層做爲佔位符來避免加載後的圖層結構發生劇烈變化。
前面已經咱們已經把非合成器動畫區分爲 Blink 觸發,沒法由合成器運行的動畫和由 Timer/RAF 驅動的 JS 動畫兩類,由於前者能夠認爲是後者的一個簡化版本,因此這一章主要討論 Timer/RAF 驅動的 JS 動畫。
從上圖能夠看出非合成器動畫的流水線比合成器動畫更長更復雜,而且非合成器動畫的後半段跟合成器動畫是一致的。
上述流程的一些關鍵點是:
總的來講對非合成器動畫性能影響最大的一般是 JavaScript 和 Rasterize,要實現高性能的非合成器動畫,頁端須要很當心地控制 JavaScript 部分的耗時,並避免在每一幀中引入大面積的網頁內容變化和大幅度的圖層結構變化。另外非合成器動畫的後半段就是合成器動畫,因此對合成器動畫的性能優化要求也一樣適應於非合成器動畫。
另外對於 WebGL 來講,當在 JavaScript 裏面調用 WebGL API 時,這些命令只是被 Chrome 緩存起來,並不會在 Renderer 線程調用真正的 GL API,因此 WebGL API 在 JavaScript 部分的耗時只是一個 JS Binding 調用的 Overhead,最終繪製 WebGL 內容的 GPU 耗時其實是被包含在最後的 GPU 的步驟裏面。可是在移動平臺上一個 JS Binding 調用的 Overhead 是至關高的,大概在 0.01 毫秒這個範圍,因此每一幀超過 1000 個 WebGL API 調用的 WebGL 遊戲,性能阻塞的瓶頸有很大機率會出如今 JavaScript 也就是 CPU 上,而不是 GPU。
文章做者: 小扎zack
原文連接:瀏覽器渲染流水線解析