從瀏覽器渲染原理談動畫效能優化

雲音樂大前端團隊發表於2022-01-18
本文作者:Bermudarat

前言

在越來越多的業務中,前端頁面除了展示資料和提供使用者操作的 UI,也需要帶給使用者更豐富的互動體驗。動畫作為承載,已經成為日常前端開發,尤其是 C 端開發的必選項。裝置硬體效能的提升、瀏覽器核心的升級也給在頁面端實現流暢動畫提供了可能。目前,常規裝置的重新整理頻率通常是 60HZ,也就是說,如果要讓使用者感受不到明顯示卡頓,瀏覽器的渲染流水線需要每秒輸出 60 張圖片(60 FPS)。
接下來,文章會從基礎的渲染樹出發,介紹瀏覽器渲染流水線,以及常用的優化動畫效能的方法。

渲染基礎

渲染樹

儘管不同的渲染引擎渲染流程不同,但是都需要解析 HTML 和 CSS 用於生成渲染樹。前端開發接觸最多的渲染引擎是 WebKit(以及在其基礎上派生的 Blink),接下來本文會以 Webkit 為基礎介紹渲染樹。

The Compositing Forest

圖片來自GPU Accelerated Compositing in Chrome

上圖中,除了我們熟悉的 DOM 樹外,還有 RenderObject 樹,RenderLayer 樹,GraphicsLayer 樹,它們共同構成了 "渲染森林"。

RenderObject

RenderObject 儲存了繪製 DOM 節點所需要的各種資訊,與 DOM 樹對應,RenderObject 也構成了一顆樹。但是RenderObject 的樹與 DOM 節點並不是一一對應關係。《Webkit 技術內幕》指出,如果滿足下列條件,則會建立一個 RenderObject

  • DOM 樹中的 document 節點;
  • DOM 樹中的可見節點(webkit 不會為非可視節點建立 RenderObject 節點);
  • 為了處理需要,Webkit 建立匿名的 RenderObject 節點,如表示塊元素的 RenderBlockRenderObject 的子類)節點。

將 DOM 節點繪製在頁面上,除了要知道渲染節點的資訊外,還需要各渲染節點的層級。瀏覽器提供了 RenderLayer 來定義渲染層級。

RenderLayer

RenderLayer 是瀏覽器基於 RenderObject 建立的。RenderLayer 最初是用來生成層疊上下文 (stacking context),以保證頁面元素按照正確的層級展示。同樣的, RenderObjectRenderLayer 也不是一一對應的,RenderObject 如果滿足以下條件,則會建立對應的 RenderLayerGPU Accelerated Compositing in Chrome):

  • 文件的根節點;
  • 具有明確 CSS 定位資訊的節點(如 relativeabsolute 或者 transform
  • 透明節點;
  • overflowmask 或者 reflection 屬性的節點;
  • filter 屬性的節點;
  • 有 3D Context 或者加速的 2D Context 的 Canvas 節點;
  • 對應 Video 元素的節點。

我們可以將每一個 RenderLayer 想象成一個圖層。渲染就是在每個 RenderLayer 圖層上,將 RenderObject 繪製出來。這個過程可以使用 CPU 繪製,這就是軟體繪圖。但是軟體繪圖是無法處理 3D 的繪圖上下文,每一層的 RenderObject 中都不能包含使用 3D 繪圖的節點,例如有 3D Contex 的 Canvas 節點,也不能支援 CSS 3D 變化屬性。此外,頁面動畫中,每次元素尺寸或者位置變動,都要重新去構造 RenderLayer 樹,觸發 Layout 及其後續的渲染流水線。這樣會導致頁面幀率的下降,造成視覺上的卡頓。所以現代瀏覽器引入了由 GPU 完成的硬體加速繪圖。

在獲得了每一層的資訊後,需要將其合併到同一個影像上,這個過程就是合成(Compositing),使用了合成技術的稱之為合成化渲染。

在軟體渲染中,實際上是不需要合成的,因為軟體渲染是按照從前到後的順序在同一個記憶體空間完成每一層的繪製。在現代瀏覽器尤其是移動端裝置中,使用 GPU 完成的硬體加速繪圖更為常見。由 GPU 完成的硬體加速繪圖需要合成,而合成都是使用 GPU 完成的,這整個過程稱之為硬體加速的合成化渲染。
現代瀏覽器中,並不是所有的繪圖都需要使用 GPU 來完成,《Webkit 技術內幕》中指出:

對於常見的 2D 繪圖操作,使用 GPU 來繪圖不一定比使用 CPU 繪圖在效能上有優勢,例如繪製文字、點、線等,原因是 CPU 的使用快取機制有效減少了重複繪製的開銷而不需要 GPU 並行性。

GraphicsLayer

為了節省 GPU 的記憶體資源,Webkit 並不會為每個 RenderLayer 分配一個對應的後端儲存。而是按照一定的規則,將一些 RenderLayer 組合在一起,形成一個有後端儲存的新層,用於之後的合成,稱之為合成層。合成層中,儲存空間使用 GraphicsLayer 表示。對於一個 RenderLayer 物件,如果沒有單獨提升為合成層,則使用其父物件的合成層。如果一個 RenderLayer 具有以下幾個特徵之一 ( GPU Accelerated Compositing in Chrome),則其具有自己的合成層:

  • 有 3D 或者透視變換的 CSS 屬性;
  • 包含使用硬體加速的視訊加碼技術的 Video 元素;
  • 有 3D Contex 或者加速的 2D Context 的 Canvas 元素;(注:普通的 2D Context 不會提升為合成層);
  • opacitytransform 改變的動畫;
  • 使用了硬體加速的 CSS filter 技術;
  • 後代包含一個合成層;
  • Overlap 重疊:有一個 Z 座標比自己小的兄弟節點,且該節點是一個合成層。

對於 Overlap 重疊造成的合成層提升,Compositing in Blink / WebCore: From WebCore::RenderLayer to cc:Layer 給出了三幅圖片:

圖 1 中,頂部的綠色矩形和底部的藍色矩形是兄弟節點,藍色矩形因為某種原因被提升為合成層。如果綠色矩形不進行合成層提升的話,它將和父節點共用一個合成層。這就導致在渲染時,綠色矩形位於藍色矩形的底部,出現渲染出錯(圖 2)。所以如果發生重疊,綠色矩形也需要被提升為合成層。

對於合成層的提升條件,無線效能優化:Composite 中有更詳細的介紹。結合 RenderLayerGraphicsLayer 的建立條件,可以看出動畫(尺寸、位置、樣式等改變)元素更容易建立 RenderLayer,進而提升為合成層(這裡要注意,並不是所有的 CSS 動畫元素都會被提升為合成層,這個會在後續的渲染流水線中介紹)。這種設計使瀏覽器可以更好使用 GPU 的能力,給使用者帶來流暢的動畫體驗。

使用 Chrome 的 DevTools 可以方便地檢視頁面的合成層:
選擇 “More tools -> Layers”

上圖中,不僅可以看到雲音樂首頁的合成層,也可以詳細看到每個合成層建立的原因。例如,頁面底部的播放欄被提升為合成層的原因是 “Overlaps other composited content”,這對應 “Overlap 重疊:有一個 Z 座標比自己小的兄弟節點,且該節點是一個合成層”。

在前端頁面,尤其是在動畫過程中,由於 Overlap 重疊導致的合成層提升很容易發生。如果每次都將重疊的頂部 RenderLayer 提升為合成層,那將消耗大量的 CPU 和記憶體(Webkit 需要給每個合成層分配一個後端儲存)。為了避免 “層爆炸” 的發生,瀏覽器會進行層壓縮(Layer Squashing):如果多個 RenderLayer 和同一個合成層重疊時,這些 RenderLayer 會被壓縮至同一個合成層中,也就是位於同一個合成層。但是對於某些特殊情況,瀏覽器並不能進行層壓縮,就會造成建立大量的合成層。無線效能優化:Composite 中介紹了會導致無法進行合成層壓縮的幾種情況。篇幅原因,就不在此文中進行介紹。

RenderObjectLayerRenderLayerGraphicsLayer 是 Webkit 中渲染的基礎,其中 RenderLayer 決定了渲染的層級順序,RenderObject 中儲存了每個節點渲染所需要的資訊,GraphicsLayer 則使用 GPU 的能力來加速頁面的渲染。

渲染流水線

在瀏覽器建立了渲染樹,會如何將這些資訊呈現在頁面上,這就要提到渲染流水線。
對於下面的程式碼:

<body>
    <div id="button">點選增加</div>
    <script>
        const btn = document.getElementById('button');
        btn.addEventListener('click', () => {
            const div = document.createElement("div");
            document.body.appendChild(div);
        });
    </script>
 </body>

在 DevTools 中的 Performance 標籤可以記錄並檢視頁面的渲染過程(所示圖片寬度限制,沒有擷取合成執行緒獲取事件輸入部分)。
chrome流水線
這個過程,與Aerotwist - The Anatomy of a Frame 給出的渲染流水線的示意圖幾乎是一致的。

渲染流水線的示意圖中有兩個程式:渲染程式(Renderer Process)和 GPU 程式(GPU Process)。
每個頁面 Tab 都有單獨的渲染程式,它包括以下的執行緒(池):

  • 合成執行緒(Compositor Thread): 負責接收瀏覽器的垂直同步訊號(Vsync,指示前一幀的結束和後一幀的開始),也負責接收滾動、點選等使用者輸入。在使用 GPU 合成情況下,產生繪圖指令。
  • 主執行緒(Main Tread): 瀏覽器的執行執行緒,我們常見的 Javascript 計算,Layout,Paint 都在主執行緒中執行。
  • 光柵化執行緒池(Raster/Tile worker):可能有多個光柵化執行緒,用於將圖塊(tile)光柵化。(如果主執行緒只將頁面內容轉化為繪製指令列表,在在此執行繪製指令獲取畫素的顏色值)。

GPU 程式(GPU Process)不是在 GPU 中執行的,而是負責將渲染程式中繪製好的 tile 點陣圖作為紋理上傳至 GPU,最終繪製至螢幕上。

下面詳細介紹下整個渲染流程:

1. 幀開始(Frame Start)

瀏覽器傳送垂直同步訊號(Vsync), 表明新一幀的開始。

2. 處理輸入事件(Input event handlers)

合成執行緒將輸入事件傳遞給主執行緒,主執行緒處理各事件的回撥(包括執行一些 Javascript 指令碼)。在這裡,所有的輸入事件(例如 touchmovescrollclick)在每一幀只會被觸發一次。

3. requestAnimiationFrame

如果註冊了 requestAnimiationFrame(rAF)函式,rAF 函式將在這裡執行。

4. HTML 解析(Parse HTML)

如果之前的操作造成了 DOM 節點的變更(例如 appendChild),則需要執行 HTML 解析。

5. 樣式計算(Recalc Styles)

如果在之前的步驟中修改了 CSS 樣式,瀏覽器需要重新計算修改的 DOM 節點以及子節點樣式。

6. 佈局(Layout)

計算每一個可見元素的尺寸、位置等幾何資訊。通常需要對整個 document 執行 Layout,部分 CSS 屬性的修改不會觸發 Layout(參考 CSS triggers)。避免大型、複雜的佈局和佈局抖動 指出,對瀏覽器幾何元計算,在 Chrome、Opera、Safari 和 Internet Explorer 中稱為佈局(Layout)。 在 Firefox 中稱為自動重排(Reflow),但實際上其過程是一樣的。

7. 更新渲染樹(Update Layer Tree)

接下來就需要更新渲染樹。DOM 節點和 CSS 樣式的改變都會導致渲染樹的改變。

8. 繪製(Paint)

實際上的繪製有兩步,這裡指的是第一步:生成繪製指令。瀏覽器生成的繪製指令與 Canvas 提供的繪製 API 很相似。DevTools 中可以進行檢視:

這些繪製指令形成了一個繪製列表,在 Paint 階段輸出的內容就是這些繪製列表(SkPicture)。

The SkPicture is a serializable data structure that can capture and then later replay commands, similar to a display list.

9. 合成(Composite)

在 DevTools 中這一步被稱為 Composite Layers,主執行緒中的合成並不是真正的合成。主執行緒中維護了一份渲染樹的拷貝(LayerTreeHost),在合成執行緒中也需要維護一份渲染樹的拷貝(LayerTreeHostImpl)。有了這份拷貝,合成執行緒可以不必與主執行緒互動來進行合成操作。因此,當主執行緒在進行 Javascript 計算時,合成執行緒仍然可以正常工作而不被打斷。

在渲染樹改變後,需要進行著兩個拷貝的同步,主執行緒將改變後的渲染樹和繪製列表傳送給合成執行緒,同時阻塞主執行緒保證這個同步能正常進行,這就是 Composite Layers。這是渲染流水線中主執行緒的最後一步,換而言之,這一步只是生成了用於合成的資料,並不是真正的合成過程。

10. 光柵化(Raster Scheduled and Rasterize)

合成執行緒在收到主執行緒提交的資訊(渲染樹、繪製指令列表等),就將這些資訊進行點陣圖填充,轉化為畫素值,也就是光柵化。Webkit 提供了一個執行緒池來進行光柵化,執行緒池中執行緒數和平臺和裝置效能有關。由於合成層每一層大小是整個頁面大小,所以在光柵化之前,需要先對頁面進行分割,將圖層轉化為圖塊(tile)。這些圖塊的大小通常是 256*256 或者 512*512。在 DevTools 的 “More tools -> Rendering” 中,選擇 “Layer borders” 可以檢視。

上圖展示了一個頁面被劃分的圖塊,橙色是合成層的邊框,青色是分塊資訊。光柵化是針對於每一個圖塊進行的,不同圖塊有不同的光柵化優先順序,通常位於瀏覽器視口(viewpoint)附近的圖塊會首先被光柵化(更詳細的可以參考 [Tile Prioritization Design
](https://docs.google.com/docum...)。現代瀏覽器裡,光柵化並不是在合成執行緒裡進行的,渲染程式維護了一個光柵化的執行緒池,也就是圖中的(Compositor Tile Workers), 執行緒池中執行緒數取決於系統和裝置相容性。

光柵化可以分為軟體光柵化(Software Rasterization)和硬體光柵化(Hardware Rasterization), 區別在於點陣圖的生成是在 CPU 中進行,之後再上傳至 GPU 合成,還是直接在 GPU 中進行繪圖和影像素填充。硬體光柵化的過程如下圖所示:

圖片來自Raster threads creating the bitmap of tiles and sending to GPU

我們可以在chrome://gpu/ 中檢視 Chrome 的硬體光柵化是否開啟。

11. 幀結束(Frame End)

圖塊的光柵化完成後,合成執行緒會收集被稱為 draw quads 的圖塊資訊用於建立合成幀(compositor frame)。合成幀被髮送給 GPU 程式,這一幀結束。

Draw quads: Contains information such as the tile's location in memory and where in the page to draw the tile taking in consideration of the page compositing.
Compositor frame: A collection of draw quads that represents a frame of a page.

12. 影像顯示

GPU 程式負責與 GPU 通訊,並完成最後影像的繪製。GPU 程式接收到合成幀,如果使用了硬體光柵化,光柵化的紋理已經儲存在 GPU 中。瀏覽器中提供了用於繪圖的 3D API(如 Webkit 的 GraphicsContext3D 類)將各紋理合並繪製到同一個點陣圖中。

前文中提過,對於設定了透明度等動畫的元素,會單獨提升為合成層。而這些變化,實際是設定在合成層上的,在紋理合並前,瀏覽器通過 3D 變形作用到合成層上,即可以完成特定的效果。所以我們才說使用 transfrom 和透明度屬性的動畫,可以提高渲染效率。因為這些動畫在執行過程中,不會改變佈局結構和紋理,也就是不會觸發後續的 Layout 和 Paint。

動畫效能優化

上面介紹了瀏覽器的渲染流水線,但是並不是每次一次渲染都會觸發整個流水線。其中的某些步驟也不一定只會被觸發一次。下面按照渲染流水線的順序,來介紹提高渲染效率的幾種方式:

合理處理頁面滾動

在瀏覽器的渲染流水線裡,合成執行緒是使用者輸入事件的入口,當使用者的輸入事件發生時,合成執行緒需要確定是否需要由主執行緒參與後續渲染。比如當使用者滾動頁面,所有圖層已經被光柵化了,合成執行緒可以直接進行合成幀的生成,並不需要主執行緒的參與。如果使用者在一些元素上繫結了事件處理,那麼合成執行緒會標記這些區域為非快速滾動區域(non-fast scrollable region)。當使用者在非快速滾動滾動區域發生輸入事件時,合成執行緒會將此事件傳遞給主執行緒進行 Javascript 計算和後續處理。
在前端開發中,經常使用事件委託這種方式將一些元素的事件委託到其父元素或者更外層的元素上(例如 document),通過事件冒泡出發其外層元素的繫結事件,在外層元素上執行函式。事件委託可以減少因為多個子元素繫結同樣事件處理函式導致的記憶體消耗,也能支援動態繫結,在前端開發中應用很廣。

如果使用事件委託的形式,在 document 上繫結事件處理,那麼整個頁面都會被標記為非快速滾動區域。這就意味合成執行緒需要將每次使用者輸入事件都傳送給主執行緒,等待主執行緒執行 Javascript 處理這些事件,之後再進行頁面的合成和顯示。在這種情況下,流暢的頁面滾動是很難實現的。

為了優化上面的問題,瀏覽器的 addEventListener 的第三個引數提供了{ passive: true }(預設為 false),這個選項告訴合成執行緒依然需要將使用者事件傳遞給主執行緒去處理,但是合成執行緒也會繼續合成新的幀,不會被主執行緒的執行阻塞,此時事件處理函式中的 preventDefault 函式是無效的。

document.body.addEventListener('touchstart', event => {
    event.preventDefault(); // 並不會阻止預設的行為
 }, { passive: true });

圖片來自Inside look at modern web browser (part 4)

此外,在例如懶載入等業務場景中,經常需要監聽頁面滾動去判斷相關元素是否處於視口中。常見的方法是使用 Element.getBoundingClientReact() 獲取相關元素的邊界資訊,進而計算是否位於視口中。主執行緒中,每一幀都呼叫 Element.getBoundingClientReact() 會造成效能問題(例如不當使用導致頁面強制重排)。Intersection Observer API 提供了一種非同步檢測目標元素與祖先元素或視口相交情況變化的方法。這個 API 支援註冊回撥函式,當被監視的元素合其他元素的相交情況發生變化時觸發回撥。這樣,就將相交的判斷交給瀏覽器自行管理優化,進而提高滾動效能。

Javascript 優化

減少主執行緒中 Javascript 的執行時間

針對於幀率為 60FPS 的裝置,每個幀需要在 16.66 毫秒內執行完畢,如果無法完成此需求,則會導致內容在螢幕上抖動,也就是卡頓,影響使用者體驗。在主執行緒中,需要對使用者的輸入進行計算,為了保證使用者的體驗,需要在主執行緒中避免長時間的計算,防止阻塞後續的流程。
優化 JavaScript 執行一文中,提出了以下幾點來優化 Javascript:

  1. 對於動畫效果的實現,避免使用 setTimeout 或 setInterval,請使用 requestAnimationFrame。
  2. 將長時間執行的 JavaScript 從主執行緒移到 Web Worker。
  3. 使用微任務來執行對多個幀的 DOM 更改。
  4. 使用 Chrome DevTools 的 Timeline 和 JavaScript 分析器來評估 JavaScript 的影響。

使用 setTimeout/setTimeInterval 來執行動畫時,因為不確定回撥會發生在渲染流水線中的哪個階段,如果正好在末尾,可能會導致丟幀。而渲染流水線中,rAF 會在 Javascript 之後、Layout 之前執行,不會發生上述的問題。而將純計算的工作轉移到 Web Worker,可以減少主執行緒中 Javascript 的執行時間。對於必須在主執行緒中執行的大型計算任務,可以考慮將其分割為微任務,並在每幀的 rAF 或者 RequestIdleCallback 中處理(參考 React Fiber 的實現)。

減少不合理 Javascript 程式碼導致的強制重排(Force Layout)

渲染流水線中,Javascript/rAF 的操作可能會改變渲染樹,進而觸發後續的 Layout。如果在 Javascript/rAF 中訪問了比如 el.style.backgroundImageel.style.offsetWidth 等佈局屬性或者計算屬性,可能會觸發強制重排(Force Layout),導致後續的 Recalc styles 或者 Layout 提至此步驟之前執行,影響渲染效率

requestAnimationFrame(logBoxHeight);
function logBoxHeight() {
  box.classList.add('super-big');
  // 為了獲取到box的offsetHeight值,瀏覽器需要在先應用super-big的樣式改變,然後進行佈局(Layout)
  console.log(box.offsetHeight);
}

合理的做法是

function logBoxHeight() {
  console.log(box.offsetHeight);
  box.classList.add('super-big');
}

減少 Layout 和 Paint

也就是老生常談的減少重排和重繪。針對渲染流水線的 Layout、Paint 和合成這三個階段,Layout 和 Paint 相對比較耗時。但是並不是所有的幀變化都需要經過完整的渲染流水線:對於 DOM 節點的修改導致其尺寸和位置發生改變時,會觸發 Layout;而如果改變並不影響它在文件流中的位置,瀏覽器不需要重新計算佈局,只需要生成繪製列表,進行 Paint。Paint 是以合成層為單位的,一旦更改了某個會觸發 Paint 的元素樣式,該元素所在的合成層都會重新 Paint。因此,所以對於某些動畫元素,可以將其提升為單獨的合成層,減少 Paint 的範圍。

合成層提升

在介紹渲染樹的時候提到滿足某些條件的 RenderObjectLayer 會被提升為合成層,合成層的繪製是在 GPU 中進行的,比 CPU 的效能更好;如果該合成層需要 Paint,不會影響其他的合成層;一些合成層的動畫,不會觸發 Layout 和 Paint。下面介紹幾種在開發中常用的合成層提升的方式:

使用transformopacity書寫動畫

上文提出,如果一個元素使用了 CSS 透明效果的動畫或者 CSS 變換的動畫,那麼它會被提升為合成層。並且這些動畫變換實際上是應用在合成層本身上。這些動畫的執行過程不需要主執行緒的參與,在紋理合成前,使用 3D API 對合成層進行變形即可。

  #cube {
      transform: translateX(0);
      transition: transform 3s linear;
  }

  #cube.move {
      transform: translateX(100px);
  }
<body>
    <div id="button">點選移動</div>
    <div id="cube"></div>
    <script>
        const btn = document.getElementById('button');
        btn.addEventListener('click', () => {
            const cube = document.getElementById('cube');
            cube.classList = 'move';
        });
    </script>
 </body>

對於上面的動畫,只有在動畫開始後,才會進行合成層的提升,動畫結束後合成層提升也會消失。這也就避免了瀏覽器建立大量的合成層造成的 CPU 效能損耗。

will-change

這個屬性告訴了瀏覽器,接下來會對某些元素進行一些特殊變換。當 will-change 設定為 opacitytransformtopleftbottomright(其中 topleftbottomright 等需要設定明確的定位屬性,如 relative 等),瀏覽器會將此元素進行合成層提升。在書寫過程中,需要避免以下的寫法:

*{ will-change: transform, opacity; }

這樣,所有的元素都會被提升為單獨的合成層,造成大量的記憶體佔用。所以需要只針對動畫元素設定 will-change,且動畫完成之後,需要手動將此屬性移除。

Canvas

使用具有加速的 2D Context 或者 3D Contex 的 Canvas 來完成動畫。由於具有獨立的合成層,Canvas 的改變不會影響其他合成層的繪製,這種情況對於大型複雜動畫(比如 HTML5 遊戲)更為適用。此外,也可以設定多個 Canvas 元素,通過合理的Canvas 分層來減少繪製開銷。

CSS 容器模組

CSS 容器模組(CSS Containment Module)最近剛釋出了Level 3版本。主要目標通過將特定的 DOM 元素和整個文件的 DOM 樹隔離開來,使其元素的更改不會影響文件的其他部分,進而提高頁面的渲染效能。CSS 容器模組主要提供了兩個屬性來支援這樣的優化。

contain

contain 屬性允許開發者指定特定的 DOM 元素獨立於 DOM 樹以外。針對這些 DOM 元素,瀏覽器可以單獨計算他們的佈局、樣式、大小等。所以當定義了 contain 屬性的 DOM 元素髮生改變後,不會造成整體渲染樹的改變,導致整個頁面的 Layout 和 Paint。
contain 有以下的取值:

layout

contain 值為 layout 的元素的佈局將與頁面整體佈局獨立,元素的改變不會導致頁面的 Layout。

paint

contain值為 paint 的 DOM 節點,表明其子元素不會超出其邊界進行展示。因此如果一個 DOM 節點是離屏或者不可見的,它的子元素可以被確保是不可見的。它還有以下作用:

  • 對於 position 值為 fixed 或者 absolute 的子節點,contain 值為 paint 的 DOM 節點成為了一個包含塊(containing block)。
  • contain 值為 paint 的 DOM 節點會建立一個層疊上下文。
  • contain 值為 paint 的 DOM 節點會建立一個格式化上下文(BFC)。

size

contain值為 size 的 DOM 節點,它的 size 不會受其子節點的影響。

style

contain值為 style 的 DOM 節點,表明其 CSS 屬性不會影響其子節點以外的其他元素。

inline-size

inline-size 是 Level 3 最新增加的值。contain 值為 inline-size 的 DOM 節點,它的 principal box的內聯軸的 intrinsic-size 不受內容影響。

strict

等同於 contain: size layout paint

content

等同於 contain: layout paint

在具有大量 DOM 節點的複雜頁面中,對沒有在單獨的合成層中的 DOM 元素進行修改會造成整個頁面的 Layout 和 Paint,此時,對這些元素設定 contain 屬性(比如 contain:strict)可以顯著提高頁面效能。

An introduction to CSS Containment 中給出了一個長列表例子,將長列表中的第一個 itemcontain 屬性設定為 strict,並改變這個 item 的內容,在此前後手動觸發頁面的強制重排。相對於沒有設定為 strict,Javascript 的執行時間從 4.37ms 降低到 0.43ms,渲染效能有了很大的提升。contain 的瀏覽器支援情況如下所示:

content-visibility

contain 屬性需要我們在開發的時候就確定 DOM 元素是否需要進行渲染上的優化,並設定合適的值。content-visibility 則提供了另外一種方式,將它設定為 auto,則瀏覽器可以自動進行優化。上文中提到,合成執行緒會對每個頁面大小的圖層轉化為圖塊(tile),然後針對於圖塊,按照一定的優先順序進行光柵化,瀏覽器會渲染所有可能被使用者檢視的元素。content-visibility 的值設定為 auto 的元素,在離屏情況下,瀏覽器會計算它的大小,用來正確展示滾動條等頁面結構,但是瀏覽器不用對其子元素生成渲染樹,也就是說它的子元素不會被渲染。當頁面滾動使其出現在視口中時,瀏覽器才開始對其子元素進行渲染。
但是這樣也會導致一個問題:content-visibility 的值設定為 auto 的元素,離屏狀態下,瀏覽器不會對其子元素進行 Layout,因此也無法確定其子元素的尺寸,這時如果沒有顯式指定尺寸,它的尺寸會是 0,這樣就會導致整個頁面高度和滾動條的顯示出錯。為了解決這個問題,CSS 提供了另外一個屬性 contain-intrinsic-size來設定 content-visibility 的值為 auto時的元素的佔位大小。這樣,即使其沒有顯式設定尺寸,也能保證在頁面 Layout 時元素仍然佔據空間。

.ele {
    content-visibility: auto;
    contain-intrinsic-size: 100px;
}

content-visibility: the new CSS property that boosts your rendering performance 給出了一個旅行部落格的例子,通過合理設定 content-visibility,頁面的首次載入效能有了 7 倍的提升。content-visibility 的瀏覽器支援情況如下所示:

總結

關於瀏覽器渲染機制,已經有大量的文章介紹。但是部分文章,尤其是涉及到瀏覽器核心的部分比較晦澀。本文從瀏覽器底層渲染出發,詳細介紹了渲染樹和渲染流水線。之後按照渲染流水線的順序,介紹了提高動畫效能的方式:合理處理頁面滾動、Javascript 優化、減少 Layout 和 Paint。希望對大家理解瀏覽器的渲染機制和日常的動畫開發有所幫助。

參考文章

  1. Webkit 技術內幕 —— 朱永盛
  2. GPU Accelerated Compositing in Chrome
  3. Compositing in Blink / WebCore: From WebCore::RenderLayer to cc:Layer
  4. 無線效能優化:Composite
  5. The Anatomy of a Frame
  6. 避免大型、複雜的佈局和佈局抖動
  7. Software vs. GPU Rasterization in Chromium
  8. 優化 JavaScript 執行
  9. 瀏覽器渲染流水線解析與網頁動畫效能優化
  10. Tile Prioritization Design
  11. CSS Containment Module Level 3
  12. Let’s Take a Deep Dive Into the CSS Contain Property
  13. CSS triggers
  14. 瀏覽器渲染詳細過程:重繪、重排和 composite 只是冰山一角
  15. 僅使用 CSS 提高頁面渲染速度
本文釋出自 網易雲音樂大前端團隊,文章未經授權禁止任何形式的轉載。我們常年招收前端、iOS、Android,如果你準備換工作,又恰好喜歡雲音樂,那就加入我們 grp.music-fe (at) corp.netease.com!

相關文章