瀏覽器渲染魔法之合成層

雲音樂大前端團隊發表於2021-12-29
本文作者:溯流

1. 前言

瀏覽器與前端開發的關係不言而喻,而瞭解瀏覽器的渲染原理,可以幫助我們提升頁面效能,解決一些渲染上的問題。最近在開發一個移動端 H5 頁面的時候,就遇到一個奇怪的問題,有一個榜單頁面在最新版本 IOS 手機上切換 tab 的時候,左上角的倒數計時出現閃爍,我們來看一些效果。

tab切換

大概的程式碼結構

tab程式碼

通過外掛檢視了一下 DOM 結構正常,樣式也和其他手機上一致,那問題出在哪裡呢?我想大概率是最新版本 IOS 瀏覽器渲染的問題。說到這種渲染問題,我第一時間想到的就是用 GPU 渲染提升為合成層試試,於是我給倒數計時的 DOM 加上了簡短的一行程式碼 will-change:transform,問題順利解決,倒數計時模組的渲染不在受其他內容的影響。為啥加了這段程式碼就是用 GPU 渲染,並且提升為合成層呢?以及合成層是什麼?讓我們一起來看看吧。

2. 瀏覽器渲染流程

在討論合成層之前我們先簡單瞭解一下瀏覽器渲染,瀏覽器常見的渲染引擎有 Webkit/Gecko 等,他們的主要渲染流程基本相同,這裡主要討論一下 WebKit 簡化的渲染流程。

渲染流程

  • 瀏覽器下載並解析 HTML。
  • 處理 CSS 構建 CSSOM 樹,生成 DOM 樹。
  • DOMCSSOM 合併成一個 Render 樹。
  • 有了 Render Tree,瀏覽器可以知道各個節點的 CSS 定義以及他們的從屬關係,從而去計算出每個節點在螢幕中的位置,生成一個足夠大的畫布來容納所有元素。
  • 根據瀏覽器提供各層的資訊合成圖層,顯示到螢幕上。

本文的主角合成層就出現在最後一步流程中,這些合成圖層中一些特殊的圖層被認為是合成層(Compositing Layers),我們來具體看看它的由來吧。

3. 關於合成層

3.1 什麼是合成層(Compositing Layer)

首先合成就是將頁面的各個部分分成多個層、單獨光柵化(瀏覽器根據文件的結構、每個元素的樣式、頁面的幾何形狀和繪製順序轉換為螢幕上的畫素的過程)它們並在合成器執行緒中合成為一個頁面的技術。

合成過程

如何去觀察頁面的圖層結構呢,您需要在 Chrome 開發工具中開啟自定義選單,然後在 More tools 中選擇 Layers 選項。

開啟圖層

這樣你就可以觀察頁面的圖層結構了,具體可以看demo

圖層結構

一般來說,擁有一些特定屬性的渲染層,會被瀏覽器自動提升為合成層。合成層擁有單獨的圖層(GraphicsLayer),和其他圖層之間無不影響。而其它不是合成層的渲染層,則和第一個擁有圖層的父層共用一個,也就是普通文件流中的內容,我們看一些常見的提升為合成層的屬性。

  • 設定 transform: translateZ(0),注意它必須是 translateZ,因為它使用 GPU 來計算 perspective distortion(透視失真)。perspective 在 3D 設計中是一個重要的屬性,有興趣的同學可以看這份資料瞭解一下。如果你使用 translateXtranslateY,元素將會被繪製在普通文件流中 demo
  • backface-visibility: hidden 指定當元素背面朝向觀察者時是否可見 demo
  • will-change 該屬性告訴瀏覽器該元素會有哪些變化,這樣瀏覽器可以提前做好對應的優化準備工作。當該屬性的值為 opacity、transform、top、left、bottom、right 時 demo
  • videocanvasiframe 等元素。

3.2 關於隱式合成

隱式合成就是特定場景下,存在會被預設提升為合成層的情況。具體我們可以再看一下之前圖層結構的 demo,只要我們把 B 和 C 的 z-index 交換一下你就會發現 B 被隱式的提升為合成層了。

隱式合成

隱式合成

只是z-index導致的麼如果我們再調整一些 B 的位置,保證 B 和 C、D 沒有交集,那麼你會發現這次 B 並沒有被隱式提升為合成層了。

不被隱式合成

所以引用 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 — i.e. painted to separate images that are then sent to the GPU.
一個或多個非合成元素應出現在堆疊順序上的合成元素之上,會被提升為合成層。

3.3 層壓縮與層爆炸

按我們剛剛說的例子,如果在堆疊順序底部有一個合成元素,那是不是會導致大量堆疊順序上的元素被提升為合成層?其實大部分情況下,我們在開發過程中並不會去關注層合成的問題,那麼我們剛剛說的情況就會有發生的可能性。當這些不符合預期的合成層達到一定量級時,就會發生層爆炸,這會導致你的頁面佔用大量的記憶體資源,帶來一些無法預期的情況。例如當 WKWebViewWKWebView 是多程式元件,這意味著會從 APP 記憶體中分離記憶體到單獨的程式中)的記憶體超過系統分配給它的記憶體的時候,瀏覽器就會崩潰白屏,但是 APP 不會 crash。這是我們不想看到的情況,面對這個問題瀏覽器也有相對應的一些解決方案,如果多個渲染層同一個合成層重疊時,這些渲染層會被壓縮到一個圖層中,以防止由於重疊原因導致出現的層爆炸
我們來看下面這段程式碼

層壓縮

B、C、D 本來都應該被提升為合成層,但是由於發生層壓縮,它們會渲染在一個圖層裡面。

層壓縮

近幾年瀏覽器在這塊的優化做的越來越好,比如我們來看一個CSS3 硬體加速也有坑文章中提供的一個有趣的 demo。頁面中包含了一個 h1 標題,它對 transform 應用了 animation 動畫,所以被提升為合成層。由於 animation transform 的特殊性(動態交疊不確定),隱式合成在不需要交疊的情況下也能發生,就導致了頁面中所有 z-index 高於它的節點所對應的渲染層全部提升為合成層,最終讓這個頁面整整產生了幾千個合成層。然後當我在自己電腦上測試這個例子的時候突然發現幾千個合成層消失了,頁面格外流暢。Why ?我的瀏覽器版本是 Chrome 96,我找了一下谷歌的歷史包,最終測試發現這個問題在 Chrome 94 Releases 版本被優化了。

谷歌瀏覽器 93 Releases 版本

animation transform

谷歌瀏覽器 96 Releases 版本

animation transform

翻看了一下 Chrome 94 的更新日誌,其中提到一條修復內容:

1238944 Medium CVE-2021-37966 : Inappropriate implementation in Compositing. Reported by Mohit Raj (shadow2639) on 2021-08-11

修復合成中不正確現象

因為對應 issues 沒有許可權訪問,有興趣的同學可以深究一下。層壓縮的存在並不代表我們可以肆無忌憚的去提升合成層,特別在一些對於渲染速度要求高的頁面,或者本身載入速度慢的頁面,我們就應該關注一下頁面的層級結構,簡化繪製的複雜度,提高頁面的效能。

4. 合成層的利弊

渲染層的提示帶來的好處:

  • 開啟硬體加速,合成層的點陣圖會交由 GPU 合成,相比 CPU 處理要快。
  • 合成層發生 repaint 的時候,不會影響其他圖層。
  • 對於 transformopacity 效果,不會觸發 layoutpaint

當然合成層也存在一些問題:

  • 如果我們把所有渲染工作都交給 GPU,在現有的優化下,它會導致渲染記憶體佔用比大幅度提升,反而出現負面的效果。
  • 另外隱式合成容易產生大量我們意料之外的合成層,過大的記憶體佔用,會讓頁面變的卡頓,效能優化適得其反。

5. 總結

5.1 使用 transform 和 opacity 來實現動畫

在我們日常開發中經常會實現一些動畫,有時候我們可能會選擇改變 top/left 去實現,那麼這個節點的渲染會發生在普通文件流中。而使用 transformopacity 實現動畫能夠讓節點被放置到一個獨立合成層中進行渲染繪製,動畫不會影響其他圖層,並且 GPU 渲染相比 CPU 能夠更快,這會讓你的動畫變的更加流暢,我們來看看他們的區別。

通過 left 來實現動畫:

left

通過 transform 來實現動畫:

transform

可以看到通過 transform 來實現動畫,頁面的 fps 能夠穩定在 60 左右,而通過 left 來實現存在波動,fps 大概穩定在 30 左右,這會影響你的使用者體驗指標。

注:檢視幀率的介面喚醒方法

幀率

如果你無法確定使用這個屬性是否合理,在你將任何 CSS 屬性用於實現動畫之前,你可以在 csstriggers 上檢視該屬性對渲染管道的影響。

csstriggers

5.2 謹慎使用 will-change

我認為除非你的元素的真的存在某個屬性馬上會發生變化,例如 transform,你可以使用 will-change: transform 告知瀏覽器,根據您打算更改的元素,瀏覽器可能可以預先安排,元素的改變和渲染速度都會變得更快。可是這些屬性可能會給你帶來一些副作用,我們來看一個demo

will-change

任何帶有 position: fixed 或者 position: absolute 的子元素將會相對於設定了 will-change: transform 的元素進行相對定位。所以在你使用的時候需要確保這種意料之外 containing block 不會對你造成影響。除此之外瀏覽器用來為 will-change 屬性做的更進一步的優化常常會耗費更多的資源,如果你將它施加在過多屬性上顯然是一個浪費,更甚者非常過度的使用可能會造成頁面相應速度的變慢或者直接崩潰。

5.3 減小合成層繪製區域

合成層的繪製區域大小,很大程度上影響了它的記憶體佔用,我們來下面這個例子:

繪製區域

可以看到 A 的尺寸是 B 的 5 倍,我們通過 transform: scale(5) 放大 B 到 200 × 200 畫素,但是它們記憶體佔用上卻相差了 25 倍之多。在使用者看不到任何區別的前提下,你能夠節省大量的記憶體。當然這個例子只適用於這種純色的場景,我們需要看到的是繪製區域對於記憶體佔有的影響。

繪製區域繪製區域

相關資料

本文釋出自 網易雲音樂大前端團隊,文章未經授權禁止任何形式的轉載。我們常年招收前端、iOS、Android,如果你準備換工作,又恰好喜歡雲音樂,那就加入我們 grp.music-fe (at) corp.netease.com!

相關文章