瀏覽器層合成與頁面渲染優化

做者:黃浩羣css

一個 CSS 屬性引起的血案

Web 頁面性能是前端開發特別須要關注的重點,評判前端 Web 頁面性能的指標有不少,頁面的流暢度是其中的一種,如何讓頁面變得 「柔順絲滑」,要討論起來可就是個至關有料的話題了。以前開發移動端 H5 頁面的時候,就遇到過一個有趣的性能問題 —— 某個賣場頁面在 IOS 手機上出現了嚴重的卡頓,但在安卓機型下卻表現得十分流暢。概括一下在 iPhoneX 上測試的具體表現:html

  • 頁面加載時存在明顯的延遲,但經過代理抓到的網絡請求耗時並不比 Android 的高;前端

  • 頁面滾動時會出現短暫的局部白屏,即丟幀。node

根據這些表徵狀況不難推斷出,應該是有什麼東西在瘋狂佔用 CPU,卡住了渲染進程。git

然而具體是什麼東西,要問我我也並不知道。對於這種無法經過斷點定位到的問題,恐怕只有用上祖師爺親傳的 「代碼二分法」 才能制服得了了。一番艱苦排查以後,問題的根源終於聚焦到了下邊這行 CSS 代碼上:github

filter: blur(100px);
複製代碼

這行 CSS 代碼用於實現一個高斯模糊,來構造一個優惠券模塊的底部陰影。因爲活動配置了多個優惠券,致使頁面裏存在多個設置了這個屬性的 div 元素,而 IOS 手機的瀏覽器彷佛對這個屬性的渲染十分吃力(然而爲什麼吃力的緣由不得而知),進而致使渲染進程的 CPU 佔用率太高,最終形成卡頓。canvas

哦?CPU 忙不過來了?好辦嘛!我給優惠券模塊又加了這樣一行代碼,而後問題迎刃而解 ......瀏覽器

will-change: transform;
複製代碼

你沒看錯,我也沒寫少,確實就是靠一行代碼解決的。性能優化

認識它的人可能已經看出來了,大體原理其實很簡單,這行代碼可以開啓 GPU 加速頁面渲染,從而大大下降了 CPU 的負載壓力,達到優化頁面渲染性能的目的,不瞭解 CSS 硬件加速的能夠看看這篇文章 Increase Your Site’s Performance with Hardware-Accelerated CSS微信

問題解決了,可是真的就這麼完事了嗎?本着 「拔樹尋根」 的偉大原則,我把這個東西好好地研究了一番,才發現 GPU 加速其實沒那麼簡單。

瀏覽器渲染流程

在具體討論原理以前,咱們須要瞭解一下瀏覽器渲染流程的一些基本概念。瀏覽器渲染流程是個老生常談的話題了,對於 「瀏覽器如何呈現一個頁面的內容」 的這類問題,很多人均可以講出一個相對完整的過程,從網絡請求到瀏覽器解析,能夠具體到不少的細節。除去網絡資源獲取的步驟,咱們理解的 Web 頁面的展現,通常能夠分爲 構建 DOM 樹構建渲染樹佈局繪製渲染層合成 幾個步驟。

  • 構建 DOM 樹:瀏覽器將 HTML 解析成樹形結構的 DOM 樹,通常來講,這個過程發生在頁面初次加載,或頁面 JavaScript 修改了節點結構的時候。

  • 構建渲染樹:瀏覽器將 CSS 解析成樹形結構的 CSSOM 樹,再和 DOM 樹合併成渲染樹。

  • 佈局(Layout):瀏覽器根據渲染樹所體現的節點、各個節點的CSS定義以及它們的從屬關係,計算出每一個節點在屏幕中的位置。Web 頁面中元素的佈局是相對的,在頁面元素位置、大小發生變化,每每會致使其餘節點聯動,須要從新計算佈局,這時候的佈局過程通常被稱爲迴流(Reflow)。

  • 繪製(Paint):遍歷渲染樹,調用渲染器的 paint() 方法在屏幕上繪製出節點內容,本質上是一個像素填充的過程。這個過程也出現於迴流或一些不影響佈局的 CSS 修改引發的屏幕局部重畫,這時候它被稱爲重繪(Repaint)。實際上,繪製過程是在多個層上完成的,這些層咱們稱爲渲染層(RenderLayer)。

  • 渲染層合成(Composite):多個繪製後的渲染層按照恰當的重疊順序進行合併,然後生成位圖,最終經過顯卡展現到屏幕上。

這是一個基本的瀏覽器從解析到繪製一個 Web 頁面的過程,跟上邊頁面卡頓問題的解決方法相關的,主要是最後一個環節 —— 渲染層合成。

渲染層合成

1、什麼是渲染層合成

在 DOM 樹中每一個節點都會對應一個渲染對象(RenderObject),當它們的渲染對象處於相同的座標空間(z 軸空間)時,就會造成一個 RenderLayers,也就是渲染層。渲染層將保證頁面元素以正確的順序堆疊,這時候就會出現層合成(composite),從而正確處理透明元素和重疊元素的顯示。

這個模型相似於 Photoshop 的圖層模型,在 Photoshop 中,每一個設計元素都是一個獨立的圖層,多個圖層以恰當的順序在 z 軸空間上疊加,最終構成一個完整的設計圖。

對於有位置重疊的元素的頁面,這個過程尤爲重要,由於一旦圖層的合併順序出錯,將會致使元素顯示異常。

2、瀏覽器的渲染原理

從瀏覽器的渲染過程當中咱們知道,頁面 HTML 會被解析成 DOM 樹,每一個 HTML 元素對應了樹結構上的一個 node 節點。而從 DOM 樹轉化到一個個的渲染層,並最終執行合併、繪製的過程,中間其實還存在一些過渡的數據結構,它們記錄了 DOM 樹到屏幕圖形的轉化原理,其本質也就是樹結構到層結構的演化。

一、渲染對象(RenderObject)

一個 DOM 節點對應了一個渲染對象,渲染對象依然維持着 DOM 樹的樹形結構。一個渲染對象知道如何繪製一個 DOM 節點的內容,它經過向一個繪圖上下文(GraphicsContext)發出必要的繪製調用來繪製 DOM 節點。

二、渲染層(RenderLayer)

這是瀏覽器渲染期間構建的第一個層模型,處於相同座標空間(z軸空間)的渲染對象,都將歸併到同一個渲染層中,所以根據層疊上下文,不一樣座標空間的的渲染對象將造成多個渲染層,以體現它們的層疊關係。因此,對於知足造成層疊上下文條件的渲染對象,瀏覽器會自動爲其建立新的渲染層。可以致使瀏覽器爲其建立新的渲染層的,包括如下幾類常見的狀況:

  • 根元素 document

  • 有明確的定位屬性(relative、fixed、sticky、absolute)

  • opacity < 1

  • 有 CSS fliter 屬性

  • 有 CSS mask 屬性

  • 有 CSS mix-blend-mode 屬性且值不爲 normal

  • 有 CSS transform 屬性且值不爲 none

  • backface-visibility 屬性爲 hidden

  • 有 CSS reflection 屬性

  • 有 CSS column-count 屬性且值不爲 auto或者有 CSS column-width 屬性且值不爲 auto

  • 當前有對於 opacity、transform、fliter、backdrop-filter 應用動畫

  • overflow 不爲 visible

DOM 節點和渲染對象是一一對應的,知足以上條件的渲染對象就能擁有獨立的渲染層。固然這裏的獨立是不徹底準確的,並不表明着它們徹底獨享了渲染層,因爲不知足上述條件的渲染對象將會與其第一個擁有渲染層的父元素共用同一個渲染層,所以實際上,這些渲染對象會與它的部分子元素共用這個渲染層。

三、圖形層(GraphicsLayer)

GraphicsLayer 實際上是一個負責生成最終準備呈現的內容圖形的層模型,它擁有一個圖形上下文(GraphicsContext),GraphicsContext 會負責輸出該層的位圖。存儲在共享內存中的位圖將做爲紋理上傳到 GPU,最後由 GPU 將多個位圖進行合成,而後繪製到屏幕上,此時,咱們的頁面也就展示到了屏幕上。

因此 GraphicsLayer 是一個重要的渲染載體和工具,但它並不直接處理渲染層,而是處理合成層。

四、合成層(CompositingLayer)

知足某些特殊條件的渲染層,會被瀏覽器自動提高爲合成層。合成層擁有單獨的 GraphicsLayer,而其餘不是合成層的渲染層,則和其第一個擁有 GraphicsLayer 的父層共用一個。

那麼一個渲染層知足哪些特殊條件時,才能被提高爲合成層呢?這裏列舉了一些常見的狀況:

  • 3D transforms:translate3d、translateZ 等

  • video、canvas、iframe 等元素

  • 經過 Element.animate() 實現的 opacity 動畫轉換

  • 經過 СSS 動畫實現的 opacity 動畫轉換

  • position: fixed

  • 具備 will-change 屬性

  • 對 opacity、transform、fliter、backdropfilter 應用了 animation 或者 transition

所以,文首例子的解決方案,其實就是利用 will-change 屬性,將 CPU 消耗高的渲染元素提高爲一個新的合成層,才能開啓 GPU 加速的,所以你也可使用 transform: translateZ(0) 來解決這個問題。

這裏值得注意的是,很多人會將這些合成層的條件和渲染層產生的條件混淆,這兩種條件發生在兩個不一樣的層處理環節,是徹底不同的。

另外,有些文章會把 CSS Filter 也列爲影響 Composite 的因素之一,然而我驗證後發現並無效果。

3、隱式合成

上邊提到,知足某些顯性的特殊條件時,渲染層會被瀏覽器提高爲合成層。除此以外,在瀏覽器的 Composite 階段,還存在一種隱式合成,部分渲染層在一些特定場景下,會被默認提高爲合成層。

對於隱式合成,CSS GPU Animation 中是這麼描述的:

This is called implicit compositing: One or more non-composited elements that should appear above a composited one in the stacking order are promoted to composite layers. (一個或多個非合成元素應出如今堆疊順序上的合成元素之上,被提高到合成層。)

這句話可能很差理解,它實際上是在描述一個交疊問題(overlap)。舉個例子說明一下:

  • 兩個 absolute 定位的 div 在屏幕上交疊了,根據 z-index 的關係,其中一個 div 就會」蓋在「了另一個上邊。

  • 這個時候,若是處於下方的 div 被加上了 CSS 屬性:transform: translateZ(0),就會被瀏覽器提高爲合成層。提高後的合成層位於 Document 上方,假如沒有隱式合成,本來應該處於上方的 div 就依然仍是跟 Document 共用一個 GraphicsLayer,層級反而降了,就出現了元素交疊關係錯亂的問題。

  • 因此爲了糾正錯誤的交疊順序,瀏覽器必須讓本來應該」蓋在「它上邊的渲染層也同時提高爲合成層。

4、層爆炸和層壓縮

一、層爆炸

從上邊的研究中咱們能夠發現,一些產生合成層的緣由太過於隱蔽了,尤爲是隱式合成。在平時的開發過程當中,咱們不多會去關注層合成的問題,很容易就產生一些不在預期範圍內的合成層,當這些不符合預期的合成層達到必定量級時,就會變成層爆炸。

層爆炸會佔用 GPU 和大量的內存資源,嚴重損耗頁面性能,所以盲目地使用 GPU 加速,結果有可能會是拔苗助長。CSS3硬件加速也有坑 這篇文章提供了一個頗有趣的 DEMO,這個 DEMO 頁面中包含了一個 h1 標題,它對 transform 應用了 animation 動畫,進而致使被放到了合成層中渲染。因爲 animation transform 的特殊性(動態交疊不肯定),隱式合成在不須要交疊的狀況下也能發生,就致使了頁面中全部 z-index 高於它的節點所對應的渲染層所有提高爲合成層,最終讓這個頁面整整產生了幾千個合成層。

消除隱式合成就是要消除元素交疊,拿這個 DEMO 來講,咱們只須要給 h1 標題的 z-index 屬性設置一個較高的數值,就能讓它高於頁面中其餘元素,天然也就沒有合成層提高的必要了。點擊 DEMO 中的複選按鈕就能夠給 h1 標題加上一個較大的 z-index,先後效果對比十分明顯。

二、層壓縮

固然了,面對這種問題,瀏覽器也有相應的應對策略,若是多個渲染層同一個合成層重疊時,這些渲染層會被壓縮到一個 GraphicsLayer 中,以防止因爲重疊緣由致使可能出現的「層爆炸」。這句話很差理解,具體能夠看看這個例子:

  • 仍是以前的模型,只不過此次不一樣的是,有四個 absolute 定位的 div 在屏幕內發生了交疊。此時處於最下方的 div 在加上了 CSS 屬性 transform: translateZ(0) 後被瀏覽器提高爲合成層,若是按照隱式合成的原理,蓋在它上邊的 div 會提高爲一個新的合成層,第三個 div 又蓋在了第二個上,天然也會被提高爲合成層,第四個也同理。這樣一來,豈不是就會產生四個合成層了?

  • 然而事實並非這樣的,瀏覽器的層壓縮機制,會將隱式合成的多個渲染層壓縮到同一個 GraphicsLayer 中進行渲染,也就是說,上方的三個 div 最終會處於同一個合成層中,這就是瀏覽器的層壓縮。

固然了,瀏覽器的自動層壓縮並非萬能的,有不少特定狀況下,瀏覽器是沒法進行層壓縮的,無線性能優化:Composite 這篇文章列舉了許多詳細的場景。

基於層合成的頁面渲染優化

1、層合成的得與失

層合成是一個相對複雜的瀏覽器特性,爲何咱們須要關注這麼底層又難理解的東西呢?那是由於渲染層提高爲合成層以後,會給咱們帶來很多好處:

  • 合成層的位圖,會交由 GPU 合成,比 CPU 處理要快得多;

  • 當須要 repaint 時,只須要 repaint 自己,不會影響到其餘的層;

  • 元素提高爲合成層後,transform 和 opacity 纔不會觸發 repaint,若是不是合成層,則其依然會觸發 repaint。

固然了,利弊是相對和共存的,層合成也存在一些缺點,這不少時候也成爲了咱們網頁性能問題的根源所在:

  • 繪製的圖層必須傳輸到 GPU,這些層的數量和大小達到必定量級後,可能會致使傳輸很是慢,進而致使一些低端和中端設備上出現閃爍;

  • 隱式合成容易產生過量的合成層,每一個合成層都佔用額外的內存,而內存是移動設備上的寶貴資源,過多使用內存可能會致使瀏覽器崩潰,讓性能優化拔苗助長。

2、Chrome Devtools 如何查看合成層

層合成的特性給咱們提供了一個利用終端硬件能力來優化頁面性能的方式,對於一些重交互、重動畫的頁面,合理地利用層合成可讓頁面的渲染效率獲得極大提高,改善交互體驗。而咱們須要關注的是如何規避層合成對頁面形成的負面影響,或者換個說法來說,更多時候是如何權衡利害,合理組織頁面的合成層,這就要求咱們事先要對頁面的層合成狀況有一個詳細的瞭解。Chrome Devtools 給咱們提供了一些工具,能夠方便的查看頁面的合成層狀況。

首先是看看頁面的渲染狀況,以一個欄目頁爲例,點擊 More tools -> Rendering,選擇 Layer borders,你就能看到頁面中的合成層都帶上了黃色邊框。

這還不夠,咱們還須要更加詳盡的層合成狀況,點擊 More tools -> Layers,你能夠看到像這樣的一個視圖:

左側列出了全部提高爲獨立合成層的元素,右側則是一個總體合成層邊界視圖,以及選定合成層的詳細狀況,包括如下幾個比較關鍵的信息:

  • Size:合成層的大小,其實也就是對應元素的尺寸;
  • Compositing Reasons:造成複合層緣由,這是最關鍵的,也是咱們分析問題的突破口,好比圖中的合成層產生的緣由就是交疊問題;
  • Memory estimate:內存佔用估算;
  • Paint count:繪製次數;
  • Slow scroll regions:緩慢滾動區域。

能夠看出咱們在不經意間就已經制造出了不少意料以外的合成層,這些沒有實際意義的合成層都是能夠被優化的。

3、一些優化建議

一、動畫使用 transform 實現

對於一些體驗要求較高的關鍵動畫,好比一些交互複雜的玩法頁面,存在持續變化位置的 animation 元素,咱們最好是使用 transform 來實現而不是經過改變 left/top 的方式。這樣作的緣由是,若是使用 left/top 來實現位置變化,animation 節點和 Document 將被放到了同一個 GraphicsLayer 中進行渲染,持續的動畫效果將致使整個 Document 不斷地執行重繪,而使用 transform 的話,可以讓 animation 節點被放置到一個獨立合成層中進行渲染繪製,動畫發生時不會影響到其它層。而且另外一方面,動畫會徹底運行在 GPU 上,相比起 CPU 處理圖層後再發送給顯卡進行顯示繪製來講,這樣的動畫每每更加流暢。

二、減小隱式合成

雖然隱式合成從根本上來講是爲了保證正確的圖層重疊順序,但具體到實際開發中,隱式合成很容易就致使一些無心義的合成層生成,歸根結底其實就要求咱們在開發時約束本身的佈局習慣,避免踩坑。

好比上邊提到的欄目頁面,就由於平時開發的不注意形成頁面生成了過多的合成層,我在試圖查看頁面合成層狀況的時候,在 PC 上已經能明顯感到卡頓了。利用 Chrome Devtools 分析以後不難發現,頁面裏邊存在的一個帶動畫 transform 的 button 按鈕,提高爲了合成層,動畫交疊的不肯定性使得頁面內其餘 z-index 大於它但其實並無交疊的節點也都所有提高爲了合成層(這個緣由真的好坑)。

這個時候咱們只須要把這個動畫節點的 z-index 屬性值設置得大一些,讓層疊順序高過於頁面其餘無關節點就行。固然並非盲目地設置 z-index 就能避免,有時候 z-index 也仍是會致使隱式合成,這個時候能夠試着調整一下文檔中節點的前後順序直接讓後邊的節點來覆蓋前邊的節點,而不用 z-index 來調整重疊關係。方法不是惟一的,具體方式仍是得根據不一樣的頁面具體分析。

改善後的頁面效果以下,能夠看到相比優化前,咱們消除了不少無心義的合成層。

三、減少合成層的尺寸

舉個簡單的例子,分別畫兩個尺寸同樣的 div,但實現方式有點差異:一個直接設置尺寸 100x100,另外一個設置尺寸 10x10,而後經過 scale 放大 10 倍,而且咱們讓這兩個 div 都提高爲合成層:

<style> .bottom, .top { position: absolute; will-change: transform; } .bottom { width: 100px; height: 100px; top: 20px; left: 20px; z-index: 3; background: rosybrown; } .top { width: 10px; height: 10px; transform: scale(10); top: 200px; left: 200px; z-index: 5; background: indianred; } </style>
<body>
  <div class="bottom"></div>
  <div class="top"></div>
</body>
複製代碼

利用 Chrome Devtools 查看這兩個合成層的內存佔用後發現,.bottom 內存佔用是 39.1 KB,而 .top 是 400 B,差距十分明顯。這是由於 .top 是合成層,transform 位於的 Composite 階段,如今徹底在 GPU 上執行。所以對於一些純色圖層來講,咱們可使用 width 和 height 屬性減少合成層的物理尺寸,而後再用 transform: scale(…) 放大,這樣一來能夠極大地減小層合成帶來的內存消耗。

參考文章

CSS3硬件加速也有坑
無線性能優化:Composite
CSS GPU Animation
詳談層合成


若是你以爲這篇內容對你有價值,請點贊,並關注咱們的官網和咱們的微信公衆號(WecTeam),每週都有優質文章推送:

WecTeam
相關文章
相關標籤/搜索