[Frontend筆記]瀏覽器視角下的前端性能優化

利用瀏覽器的渲染原理,對前端性能優化進行解析

Posted by 李定宇 on Wednesday, October 26, 2022

瀏覽器渲染流程的觀點下,前端性能優化的原則

前言

瀏覽器輸入URL後發生什麼事一文中,我們可以得知在瀏覽器從 Network Process 接收到HTML數據後,會交給 Render Process 來進行渲染;而在前端工程角度下的性能優化,便是考慮在 Render Process 渲染中、如何對程式碼進行調優,進而加快渲染速度。因此,了解 Render Process 是如何對頁面進行渲染,在前端性能優化中是十分重要的!

Browser Rendering pipeline(渲染流水線)

在瀏覽器的渲染流水線中,基本上可以分為以下步驟:

  1. 建構DOM樹
  2. 計算樣式
  3. 佈局
  4. 分層
  5. 繪製
  6. 合成(分塊、栅格化、合成)

而基於pipeline的結構、也就是一個步驟的輸出是下一個步驟的輸入,因此,只要關注每個步驟輸入了什麼、然後輸出了什麼到下一個步驟,就可以很清楚的了解整個 Render Pipeline 的流程:

  • 建構DOM樹

    它的輸入是一個最簡單的HTML文件,然後經由Render Process 中的 HTMLParse模塊,解析成樹狀結構的DOM Tree,然後輸出到下一階段—計算樣式。

  • 計算樣式

    此階段的目的是要計算出每個DOM節點的樣式。Render Process接收到CSS文件時(無論是通過<style></style><link ref=""/>還是inline的方式)後,會將其轉換為styleSheets以讓瀏覽器理解。然後計算每個DOM的樣式(如繼承、單位統一等等),保存在 ComputedStyle結構中,輸出到下一階段—佈局

  • 佈局

    結合DOM TreeComputedStyle,然後摒除一些不可見的元素(如<head>, display:none等),接著計算各個DOM在頁面中的位置,形成Layout Tree輸出給分層階段

  • 分層

    頁面中有一些複雜的畫面效果(如css中的opacityz-index、3D效果等等),因此Render Process還需要為一些節點生成專門的圖層(layer),形成Layer Tree,然後輸出給繪製階段

  • 繪製

    在這一階段,Render Process是基於Layer Tree,來生成相對應的繪製指令(白話文來說就像是「在座標(100,30)的地方畫藍色」等等),將這些指令列表提交到合成線程(threading)中。

  • 合成線程

    將繪製指令基於(viewport)分成一塊塊的圖塊(讓瀏覽器不用一下全部渲染),然後再基於該圖塊的附近優先生成位圖。這個過程通常會讓GPU Process參與來加速渲染流程。接著,合成線程發送繪製圖塊命令 DrawQuad給Browser Process.

  • 最後,Browser Process根據接收到的 DrawQuad 消息來生成頁面,顯示到顯示器上。

基於渲染流水線的性能優化

了解了渲染流水線的大致過程,便可以如庖丁解牛般,對每個可以操作的步驟來進行優化、進而達到全面的性能優化!而以瀏覽器的角度出發,可以把性能優化再分為 加載階段的優化交互階段的優化

加載階段的優化

圖片、影片等資源,是不會阻塞頁面的首次渲染,但是Javascript文件和CSS文件會。因為在建構DOM樹的時候,HTMLParse遇到<script>標籤後,會暫停渲染並執行該script;而在構造Layout Tree時會需要CSS文件。(另外,如果JS文件中有會修改到CSS屬性,也必須等待CSS文件加載完後、構建CSSOM後才能執行該JS script)

  • Javascript文件優化:
    • 使用CDN加速(因為同一個站點,Network Process能給執行的TCP有限(6個?);而有CDN的話就可以規避這個侷限)
    • 壓縮JS文件體積(webpack 插件)
    • 如果JS文件中沒有操作DOM的script,可以用異步加載(deferasync。兩者的差別是:async是文件加載完後立刻執行,而defer是要等DOM建立完之後(DOMContentLoaded)執行)
  • CSS文件優化

    • 大的CSS文件,可以拆分成不同用途的CSS文件,然後在特定情況下加載特定CSS文件
    • 利用分層技術來優化。如果直接修改CSS,如JS對某個DOM進行幾何形狀變化、透明度變化或縮放操作等等,會影響整個渲染流水線(Reflow/Repaint);

      • Reflow—因為修改了DOM的幾何屬性如大小,所以瀏覽器要重新計算樣式、佈局、分層,等於要從計算樣式的階段重新開始渲染流水線。
      • Repaint—修改了DOM的顏色,雖然也要重新計算樣式、但不用再重新佈局和分層,所以會在渲染流水線中跳過這兩個階段,但還是耗性能。
      • 但如果在css文件中加上will-change屬性來告訴Render Process這個元素會做特殊變化,那麼Render Process會將這個變化放到合成線程中去執行,而合成線程不會造成整個渲染流水線的 Reflow 或 Repaint

        .box {
        will-change: transform, opacity, background-color;
        }
        

交互階段的優化

在交互階段,是頁面加載完成到使用者交互的過程,這裏最主要的就是Javascript

  • 減少Javascript的執行時間
    • 將一個大型的Javascript任務拆解成多個小任務。如果一個JS任務長期霸佔main thread,會讓用戶體驗變很差。
      • 如:spa router 的 lazy-loading
    • 使用web workers,將不會和DOM相關、但又耗時的JS script放到 web workers來執行。
  • 避免強制同步佈局和佈局抖動
    • 強制同步佈局,簡單來說就是在一次操作中,既要計算樣式又要改變佈局,如在 function foo中,要把.testDom新增子節點、又要查詢.testDom的offsetHeight
    • 佈局抖動,最有可能發生的情況是在一個函數中多次強制同步佈局
    • 虛擬DOM,在某種程度上也是解決了JS多次操作DOM的問題。在JS環境中先修改完虛擬DOM,然後在渲染成真實DOM,這樣就只要修改一次就好。
  • 避免頻繁垃圾回收
    • 像是在一段for循環,如果有共用變量或對象的話,那就在for循環外聲明而不是循環內,這樣就不用每結束一次循環就要回收一次。

ChangeLog

  • 20221026 - 初稿

Ref