詳解 CRP:如何最大化提高首屏渲染速度

在前端性能優化樹上有不少值得展開的話題,從輸入 URL 到頁面加載完成發生了什麼 這一道經典的面試題就涉及到不少內容,但前端主要關注的部分仍是 瀏覽器解析響應的內容並渲染展現給用戶 這一步,本文將會詳細分析這一步的具體過程並在分析的過程當中理解該如何作性能優化。css

首先介紹一個名詞 CRP,即 關鍵渲染路徑 (Critical Rendering Path)(後文統一以 CRP 指代):html

關鍵渲染路徑是瀏覽器將 HTML CSS JavaScript 轉換爲在屏幕上呈現的像素內容所經歷的一系列步驟。前端

將 HTML 轉換成 DOM 樹

當咱們請求某個 URL 之後,瀏覽器得到響應的數據並將全部的標記轉換到咱們在屏幕上所看到的 HTML,有沒有想過這中間發生了什麼?git

瀏覽器會遵循定義好的完善步驟,從處理 HTML 和構建 DOM 開始:github

  • 瀏覽器從磁盤或網絡中讀取 HTML 原始字節,並根據文件的指定編碼將它們轉成字符。
  • 當遇到 HTML 標記時,瀏覽器會發出一個令牌,生成諸如 StartTag: HTML StartTag:head Tag: meta EndTag: head 這樣的令牌 ,整個瀏覽由令牌生成器來完成。
  • 在令牌生成的同時,另外一個流程會同時消耗這些令牌並轉換成 HTML head 這些節點對象,起始和結束令牌代表了節點之間的關係。
  • 當全部的令牌消耗完之後就轉換成了DOM(文檔對象模型)。

DOM 是一個樹結構,表示了 HTML 的內容屬性以及各個節點之間的關係。web

ToDOM

好比如下代碼:面試

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link href="style.css" rel="stylesheet">
    <title>Critical Path</title>
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg"></div>
  </body>
</html>
複製代碼

最終就轉成下面的 DOM 樹:瀏覽器

DOM

瀏覽器如今有了頁面的內容,那麼該如何展現這個頁面自己呢?緩存

將 CSS 轉換成 CSSOM 樹

與轉換 HTML 相似,瀏覽器首先會識別 CSS 正確的令牌,而後將這些令牌轉成 CSS 節點,子節點會繼承父節點的樣式規則,這就是層疊規則和層疊樣式表。性能優化

ToCSSOM

好比上面的 HTML 代碼有如下的 CSS :

body { font-size: 16px }
p { font-weight: bold }
span { color: red }
p span { display: none }
img { float: right }
複製代碼

最終就轉成下面的 CSSOM 樹:

CSSOM

這裏須要特別區分的是,DOM 樹會逐步構建來使頁面更快地呈現,可是 CSSOM 樹構建時會阻止頁面呈現

緣由很簡單,若是 CSSOM 樹也能夠逐步呈現頁面的話,那麼以後新生成的子節點樣式規則有可能會覆蓋以前的規則,這就會致使頁面的錯誤渲染。

讓咱們來作一個思考題,請看如下的 HTML 代碼:

<div>
    <h1>H1 title</h1>
    <p>Lorem...</p>
</div>
複製代碼

對於如下兩個樣式規則,哪一個樣式規則會渲染得更快?

h1 { font-size: 16px }
div p { font-size: 12px }
複製代碼

直覺上很容易以爲第二個規則是更具體的,應該會渲染更快,但實際上偏偏相反:

  • 第一條規則是很是簡單的,一旦遇到 h1 標記,就會將字號設成 16px。
  • 第二條規則更復雜,首先它規定了咱們應該知足全部 p 標記,可是當咱們找到 p 標記時,還須要向上遍歷 DOM 樹,只有當父節點是 div 時纔會應用這個規則。
  • 因此更加具體的標記要求瀏覽器處理的工做更多,實際編寫中應該儘量避免編寫過於具體的選擇器。

那麼到如今爲止,DOM 樹包含了頁面的全部內容,CSSOM 樹包含了頁面的全部樣式,接下來如何將內容和樣式轉成像素顯示到屏幕上呢?

將 DOM 和 CSSOM 樹組成渲染樹

瀏覽器會從 DOM 樹的根部開始看有沒有相符的 CSS 規則,若是有的話就將節點和樣式複製到渲染樹上,沒有的話就只將節點複製過來,而後繼續向下遍歷。

特別要注意的是,渲染樹最重要的特性是只捕獲可見內容 :

  • 對於特殊節點(html head)等,由於它們不會被渲染,所以會直接跳過。
  • 若是一個節點的屬性標記爲 display: none,表示這個節點不該該呈現,則這個節點和其子項都會直接跳過。

好比如下將 DOM 樹和 CSSOM 樹合併成渲染樹的結果:

渲染樹

如今咱們已經有了渲染樹,接下來要作的是肯定元素在頁面上的位置。

佈局與繪製

咱們考慮如下的代碼:

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>Critial Path: Hello world!</title>
  </head>
  <body>
    <div style="width: 50%">
      <div style="width: 50%">Hello world!</div>
    </div>
  </body>
</html>
複製代碼

瀏覽器在渲染時會將這裏父 div 的寬度設置成 body 的 50%,將子 div 的寬度設成父 div 的 50%,那麼這裏 body 的寬度是如何肯定的?

注意咱們在 meta 標籤中設置了一行代碼:

<meta name="viewport" content="width=device-width,initial-scale=1">
複製代碼

咱們在實際進行自適應網頁設計時都會加上這行代碼表示佈局視口的寬度等於設備的寬度,所以呈現出來就是這樣:

viewport

最後一步就是將全部準備好的內容 繪製 到頁面上。

任什麼時候候咱們想要更新渲染樹時,可能都會從新進行佈局和繪製這一過程,瀏覽器自己會採起各類智能的功能嘗試從新繪製最低請求區域,但具體仍是取決於咱們向渲染樹應用了哪一種類型的更新。

如何優化

在談優化以前,咱們先定義一下用來描述 CRP 的詞彙:

  • 關鍵資源: 可能阻止網頁首次渲染的資源。
  • 關鍵路徑長度: 獲取全部關鍵資源所需的往返次數或總時間。
  • 關鍵字節: 實現網頁首次渲染所需的總字節數,等同於全部關鍵資源傳送文件大小的總和。

結合咱們談過的步驟,咱們着重會考慮的優化策略是在合成渲染樹以前。

首先咱們能夠優化 DOM,具體體如今如下幾步:

  • 刪除沒必要要的代碼和註釋包括空格,儘可能作到最小化文件。
  • 能夠利用 GZIP 壓縮文件。
  • 結合 HTTP 緩存文件。

而後是優化 CSSOM,縮小、壓縮以及緩存一樣重要,對於 CSSOM 咱們前面重點提過了它會阻止頁面呈現,所以咱們能夠從這方面考慮去優化,讓咱們看下面的代碼:

body { font-size: 16px }
@media screen and (orientation: landscape) {
    .menu { float: right }
}
@media print {
    body { font-size: 12px }
}
複製代碼

當瀏覽器遇到 CSS 時,會阻止呈現頁面直到 CSSOM 解析完畢,可是對於一些特定場合纔會運用的 CSS (好比上面兩個媒體查詢),瀏覽器會依舊請求,但不會阻塞渲染了,這也是爲何咱們有時會將 CSS 文件拆分到不一樣的文件,上面的樣式表聲明能夠優化成這樣:

<link href="style.css"    rel="stylesheet">
<link href="landscape.css" rel="stylesheet" media="orientation:landscape">
<link href="print.css"    rel="stylesheet" media="print">
複製代碼

當咱們用 PageSpeed Insights 檢測咱們的網站時,常常出現的一條就是 建議減小關鍵 CSS 元素數量

Google 官方文檔 也建議: 當咱們聲明樣式表時,請密切關注媒體查詢的類型,它們極大地影響了 CRP 的性能

接下來讓咱們考慮 JavaScript 外部依賴能夠優化的地方,再看下面的代碼:

<p>
    Awesome page
    <script src="write.js"></script>
    is awesome
</p>
複製代碼

當瀏覽器遇到 script 標記時,會阻止解析器繼續操做,直到 CSSOM 構建完畢JavaScript 纔會運行並繼續完成 DOM 構建過程,對於 JavaScript 依賴的優化,咱們最經常使用的一種方法是當網頁加載完成,瀏覽器發出 onload 事件後再去執行腳本(或者直接放在底部),但實際上還有更簡單的策略:

  • async: 當咱們在 script 標記添加 async 屬性之後,瀏覽器遇到這個 script 標記時會繼續解析 DOM,同時腳本也不會被 CSSOM 阻止,即不會阻止 CRP。
  • defer: 與 async 的區別在於,腳本須要等到文檔解析後( DOMContentLoaded 事件前)執行,而 async 容許腳本在文檔解析時位於後臺運行(二者下載的過程不會阻塞 DOM,但執行會)。
  • 當咱們的腳本不會修改 DOM 或 CSSOM 時,推薦使用 async

這裏給出一個參考圖:

render

瀏覽器還有一個特殊的流程,叫作預加載掃描器,它會提早掃描文檔並發現關鍵的 CSS 和 JS 資源來下載,這個過程不會阻塞渲染,想詳細瞭解它的原理能夠瀏覽這篇文章 How the Browser Pre-loader Makes Pages Load Faster,實際的應用可瀏覽 前端性能優化之關鍵路徑渲染優化

總結一下,爲了首屏最快地渲染,咱們一般會採起下列步驟:

  • 分析並用 關鍵資源數 關鍵字節數 關鍵路徑長度 來描述咱們的 CRP 。
  • 最小化關鍵資源數: 消除它們(內聯)、推遲它們的下載(defer)或者使它們異步解析(async)等等 。
  • 優化關鍵字節數(縮小、壓縮)來減小下載時間 。
  • 優化加載剩餘關鍵資源的順序: 讓關鍵資源(CSS)儘早下載以減小 CRP 長度 。

更詳細的優化建議能夠閱讀 PageSpeed Rules and Recommendations

參考

相關文章
相關標籤/搜索