前端不止:Web效能優化–關鍵渲染路徑以及優化策略

李博bluemind發表於2018-06-03

我問你:“當你從搜尋引擎的結果頁面選擇開啟一條搜尋結果時,你覺得多長時間之後,如果頁面還處於白屏或者沒有載入到關鍵資訊,你會選擇關掉這個視窗?”

《Designing for Performance》的作者 Lara Swanson 在2014年寫過一篇文章《Web效能即使用者體驗》,她在文中提到“網站頁面的快速載入,能夠建立使用者對網站的信任,增加回訪率,大部分的使用者其實都期待頁面能夠在2秒內載入完成,而當超過3秒以後, 就會有接近40%的使用者離開你的網站”。

Google和亞馬遜的研究表明,Google頁面載入的時間從0.4秒提升到0.9秒導致丟失了20%流量和廣告收入,對於亞馬遜,頁面載入時間每增加100毫秒就意味著1%的銷售額損失。可見,頁面的載入速度對於使用者可能的下一步操作是多麼的舉足輕重。

想一想,如果你希望你的網站在一秒鐘之內呈現使用者想看的關鍵資訊,有哪些可行的手段?Minify,壓縮,雪碧圖等等。

Google的Web效能工程師 Ilya Grigorik 會告訴你,你只需要理解瀏覽器的關鍵渲染路徑。

頁面效能可能是一個感性的東西

頁面的效能,看似是一個理性和量化的概念,實則也來自於使用者的感知,主觀的評價,是一個偏感性的東西。

(參考自Google關鍵渲染路徑)

如果頁面可以做到優先顯示與使用者操作有關的內容,就可以讓使用者更快速的感知到操作得到響應,這個過程叫做“優化關鍵渲染路徑”。

什麼是關鍵渲染路徑

我記得,有一個非常經典的面試題叫做:《當瀏覽器位址列輸入URL並回車後,發生了什麼?》。

關鍵渲染路徑就是描述瀏覽器從收到 HTML、CSS 和 JavaScript 位元組開始,到如何使用HTML、CSS 和 JavaScript 在螢幕上渲染畫素的中間過程。

如果我們能夠優化這條路徑,就能讓頁面更快速的展示內容,給使用者更好的體驗。

全景圖

我們先嚐試站在高處,看一眼關鍵渲染路徑的全景圖,這樣能夠快速的領略一個大致輪廓和一些關鍵概念。

文件物件模型 (DOM)

DOM概念之於Web開發人員再熟悉不過了,當瀏覽器發出請求並接收到HTML文件後,它會有這樣一個流程來構建DOM:位元組 → 字元 → 令牌 → 節點 → 物件模型。

以下面這段程式碼為例:

<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>

瀏覽器接收到HTML請求的返回結果,根據預定的流程解析HTML,文件中的“開標籤”,比如<html><head>等會轉換成一個令牌(Token),然後令牌轉換成節點物件(Node)。

這個令牌解析並轉換為節點物件的過程,也是每個節點建立關係(樹形結構)的過程。例如:head的令牌出現在html令牌之後,但其閉標籤出現在html閉標籤之前,這就意味著headhtml的子節點,以此類推,建立節點的父子關係。

這個過程在瀏覽器中,叫做“Parse HTML”。

CSS 物件模型 (CSSOM)

當DOM捕獲了頁面的內容,我們還需要知道頁面如何展示這些內容,所以需要構建CSS 物件模型(CSSOM)。

瀏覽器解析DOM,遇到了link標籤,發現它引用了一個外部樣式資源:style.css,於是瀏覽器會向外部請求樣式資源,然後進行後續的DOM構建工作。

CSS 被視為阻塞渲染的資源,這意味著瀏覽器將不會渲染任何已處理的內容,直至 CSSOM 構建完畢。

CSSOM有著一個和DOM構建相似的流程:位元組 → 字元 → 令牌 → 節點 → CSS物件模型。

以下面的CSS樣式為例,它會根據具體解析規則,將CSS文件轉換成下面的樹形結構:

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

這種樹形結構讓CSS有層級繼承關係,子節點會繼承父節點的樣式。

前面談到CSS會阻塞瀏覽器的渲染過程,因為渲染樹的構建同時需要DOM和CSSOM,所以當瀏覽器請求的style.css返回之後,瀏覽器會開始解析樣式表,並重新計算樣式(Recalculate Style),將CSS轉換成CSSOM,然後進行後續的操作。

值得注意的是,CSSOM運算是一個非常複雜的過程,效能消耗會比較大,所以你會常常聽到“老人們”說寫樣式“儘量使用classid,保證層級扁平,減少過度層疊”,而且越是通用的CSS樣式,執行速度越快,越是具體(選擇器)的CSS樣式,則執行速度越慢。

DOM + CSSOM = 渲染樹

渲染樹和DOM樹不同,它只會捕獲一些頁面上可見的元素,比如,Headerdisplay:none的元素不會放在渲染樹中。

渲染樹的構建會從DOM的根節點開始遍歷,對於不可見節點會忽略,然後在CSSOM中找到每個對應節點的樣式規則並應用,最後輸出的渲染樹會包含所有的可見內容和樣式資訊,如下圖:

佈局和繪製

有了渲染樹,瀏覽器會進入佈局和繪製階段。

佈局就是弄清每個物件在頁面視窗(Viewport)上的確切大小和位置,它的輸出是一個“盒模型”,裡面準確的捕獲每一個元素在頁面視窗中的位置和尺寸。

在佈局工作完成之後,瀏覽器會開始繪製,將渲染樹轉換成螢幕上的畫素,這樣,我們就能在瀏覽器中看到頁面的內容。

短暫回顧一下“關鍵渲染路徑”的步驟

  1. 處理 HTML 標記並構建 DOM 樹。
  2. 處理 CSS 標記並構建 CSSOM 樹。
  3. 將 DOM 與 CSSOM 合併成一個渲染樹。
  4. 根據渲染樹來佈局。
  5. 將各個節點繪製到螢幕上。

當DOM或者CSSOM發生變化的時候,瀏覽器就需要再次執行一次上面的步驟。

JavaScript

到目前為止,我們還沒涉及到JavaScript,但它在整個關鍵渲染路徑中扮演著非常重要的角色,就如全景圖中畫的那樣,我們從一段簡單的程式碼開始:

<body>
 <p>Hello <span>web performance</span> students!</p>
 <script>
 var span = document.getElementsByTagName(`span`)[0];
 span.textContent = `javascript`;
 </script>
</body>

一個大家都知道的重要事實是:指令碼在文件的何處插入,就在何處執行。

當HTML解析過程中遇到一個script標記時,它會暫停DOM構建,將控制權移交給JavaScript引擎,等JavaScript引擎執行完畢,瀏覽器再從中斷的地方恢復DOM構建。也就是說,執行內聯的JavaScript會阻塞頁面的首次渲染。

現在我們假設,這段JavaScript是外部資源。

<body>
 <p>Hello <span>web performance</span> students!</p>
 <script src="write.js"></script>
</body>

則瀏覽器的渲染會阻塞直到write.js的請求返回後,並執行JavaScript後,繼續。

需要注意的是,在網頁中引入JavaScript指令碼有一個微妙事實,就是JavaScript不僅可以讀取和修改DOM屬性,還可以讀取和修改CSSOM屬性。

前面我們提到CSS是阻塞渲染的資源,當它和JavaScript一起出現在頁面上時,會發生這樣的事情:

<html>
 <head>
 <meta name="viewport" content="width=device-width,initial-scale=1">
 <link href="style.css" rel="stylesheet">
 <title>Critical Path: Script</title>
 </head>
 <body>
 <p>Hello <script>document.write(`web performance`)</script> students!</p>
 </body>
</html>

在瀏覽器解析HTML構建DOM過程中,發現了link標籤,於是發出請求獲取style.css,然後繼續構建DOM,此時,它發現script標籤,由於JavaScript可能會訪問樣式屬性,所以它會阻止JavaScript的執行直到styles.css返回並完成CSSOM構建,然後執行這一段JavaScript程式碼,再繼續後面DOM的構建和相關渲染操作。

於是styles.css的請求不僅阻塞後面的渲染,還阻塞了DOM的構建。

如果將這段JavaScript作為外部資源,就是一個比較典型的頁面結構:

<html>
 <head>
 <meta name="viewport" content="width=device-width,initial-scale=1">
 <link href="style.css" rel="stylesheet">
 <title>Critical Path Render</title>
 </head>
 <body>
 <p>Hello <span>web performance</span></p>
 <script src="app.js"></script>
 </body>
</html>

JavaScript和CSS資源請求是並行的,但仍然需要等到CSSOM構建完成之後,JavaScript才可以執行,然後在進行後面的渲染工作。於是,當 DOM、CSSOM 和 JavaScript 執行之間有大量的依賴關係時,就很可能導致瀏覽器在處理及渲染網頁時出現延遲。

優化策略

我們花了大量的篇幅來理解瀏覽器的渲染過程,理解DOM,CSSOM,渲染樹,瀏覽器繪製,分析HTML,CSS和JS在渲染過程中的關係,我相信你已然受益匪淺,現在,我們來運用這些知識加速你的網站。

第一步,分析你的網站渲染狀況

我們以Google為例,通過Chrome的Performance工具檢視頁面渲染情況,如下圖,你應該可以清晰的看到圖中有四條豎線,他們分別是什麼含義呢?

(Google主頁的效能分析情況)

  1. 綠色豎線,代表First Paint,即瀏覽器開始進行畫素的繪製
  2. 黃色豎線,代表First Meaningful Paint(首次有效繪製)使用者可以開始看到部分內容,但繪製仍在繼續
  3. 藍色豎線,代表大家比較熟悉的DOMContentLoaded
  4. 紅色豎線,代表load,頁面載入完成

優秀的網站都能夠把“首次有效渲染”做到1秒之內完成,這樣能夠讓使用者更快的看到所請求的頁面得到響應。如果你的網站“首次有效渲染”超過1秒,那麼就非常有必要重新分析一下網站的關鍵渲染路徑是否合理。

第二步,分析關鍵渲染路徑

在關鍵渲染路徑中,我們通常要關注三個點:

  1. 頁面首次渲染需要的關鍵資源數量
  2. 關鍵資源的大小
  3. 關鍵渲染路徑的往返次數(Roundtrip)

我們的策略也非常簡單,就是減少關鍵資源數量,降低資源大小,減少關鍵路徑的往返次數。

關鍵渲染的資源一般是阻止螢幕首次渲染HTML,CSS和JavaScript,所以最重要也是最難的部分的是你需要根據自己網站的實際情況分析,哪些是頁面繪製的所必須的,哪些是無關的。

第三步,根據分析採取優化手段

1、減少關鍵資源的大小

我們首先從最簡單也是最直接的減少關鍵資源的大小開始:

對於所有的資源(HTML,JavaScript,CSS,Image等),你都應該用上三大絕招:Minify,Compression和Cache,這裡不過多的贅述裡面的細節。

這一點對於HTML來說,非常關鍵,HTML作為渲染的關鍵資源,消除或者延遲載入肯定不太可能(這裡指的是非區域性渲染的關鍵HTML),能夠做到是消除無用程式碼(比如:註釋)和最小化程式碼(Minify)以及動態區域性渲染等。

(Google對頁面的HTML進行了壓縮)

2、延遲JavaScript非阻塞資源載入

JavaScript和CSS都是阻塞渲染的資源,對於已經鑑別出的對於首次渲染沒有起到關鍵作用的程式碼,我們首先想到的是要延遲它的載入,讓它脫離關鍵渲染路徑。

首先,對於阻塞渲染的JavaScript,應該將它放置在頁面body的底部,為什麼呢?

JavaScript可以查詢和操作DOM和CSSOM,正如前面介紹的,HTML解析過程中構建DOM,當遇到JavaScript就停止DOM構建執行JavaScript,如果被執行的JavaScript是放置在head附近,那麼很可能要被操作或者查詢的DOM還沒有構建到DOM當中。

而對於,非阻塞渲染的JavaScript,我們應該採用非同步的方式載入,如下:

 <script src="script.js"></script>
 <script async src="script.js"></script>
 <script defer src="myscript.js"></script>
</body>
</html>

方式一:即阻塞的JavaScript,HTML解析過程中遇到script標籤,發出網路請求獲取script.js,在網路請求返回後,解析並執行script.js,然後瀏覽器繼續HTML解析。

方式二:async,完全的非同步操作,HTML解析遇到該標籤後,發出網路請求,但不阻止HTML解析和其後面的渲染操作,當JavaScript請求返回後立刻執行,且不等待HTML解析或其他操作的完成。所以,如果指令碼中有DOM操作,就並不適合。比較適合的場景是Google Analytics。

方式三:defer,HTML的解析和對JavaScript資源的網路請求是並行的,但它會等待HTML解析完成之後,才執行指令碼。

(圖片參考自:Asynchronous and deferred JavaScript execution explained « Peter Beverloo)

不過,async和defer,他們對瀏覽器的相容性有一定的要求,但仍然應該使用它們,同時可以採用退而求其次的延遲程式碼執行的方法(比如:on DOMContentLoad後),特別是與首次渲染無關的計算邏輯和功能。

3、儘早和按需的載入CSS

你可能在思考,有沒有非同步載入CSS的需求?我認為不應該有,頁面應該只引用與該頁面相關的樣式檔案。(只不過很多時候,我們將所有的CSS都打包在了一個壓縮的CSS檔案中了。)目前,已經有許多幫助你分析關鍵渲染路徑上的所需要的CSS的工具:grunt-criticalcsscriticalcriticalCSS,線上CRPCSS工具。

前面已經提到,CSS是阻塞渲染的資源,在CSSOM完全解析完成之前,瀏覽器不可能開始螢幕的繪畫。

所以,我們應該儘早的開始對樣式資源的請求,將它儘早、儘快地下載到客戶端,這樣解釋了為什麼我們看到樣式資源的link標籤一般都放在head表中:

<!DOCTYPE html>
<html lang="en">
<head>
 <title>Sample Site</title>
 <link href="style.css" rel="stylesheet">
</head>

CSSOM的運算是一個非常複雜和相對耗時的過程,但它也有一個特點,就是可能只有在特定的情況下才會起作用,比如:響應式設計的頁面。

對於響應式頁面,我們可以考慮將不同媒體上的樣式分離,在<link>中使用媒體查詢,瀏覽器仍然會下載對應的資源,但是可以避免不必要的CSSOM解析導致對渲染的阻塞。

<link href="style.css" rel="stylesheet" media="all">
<link href="portrait.css" rel="stylesheet" media="orientation:portrait">
<link href="print.css" rel="stylesheet" media="print">

同時,我們還應該避免在首次渲染的CSS樣式中使用@import指令,因為它只有在收到並解析完帶有@import規則的CSS資源之後,才會發現匯入的 CSS 資源,這個時候就會重新請求,從而增加了關鍵渲染路徑的往返次數。

4、內聯CSS來提高渲染效能

到目前為止,我們已經做到了識別關鍵渲染資源,將非關鍵資源延遲載入或者不載入。那麼,減少關鍵路徑的往返次數是什麼意思?其實就是減少關鍵渲染資源從伺服器端到客戶端的往返次數。比如,外鏈的JS和CSS檔案以前CSS的@import,在頁面渲染的過程中,都會重新去伺服器端請求。這其實,和我們常說的減少http請求量(合併http請求)類似,但是我麼從渲染路徑的角度來理解這樣一種效能的消耗。

根據這樣的邏輯,我們很容易就想到可以將渲染必備CSS內聯到HTML中,來減少渲染路徑的往返次數。

實際上不少的優秀網站都採用了在head內聯樣式的做法:Google,百度,淘寶,京東。

(百度和Google將樣式inline在head中)

關於內聯樣式還有更進一步的做法,在文章的一開始就提到,優化關鍵渲染路徑就是要優先顯示和使用者先關內容。

所以,我們可以考慮僅僅將當前螢幕展示的內容(above-the-fold,一屏)所需的CSS內聯到HTML的head中,然後採用非同步的方式載入整個頁面所需要的完整CSS,以便使用者能夠更快的看到首屏出現的內容。(inlining-critical-css-for-better-web-performance

5、一個神奇的數字14kb

在最開始我們提到,要減小關鍵資源的大小,那麼多小比較合適呢?(廢話,當然是越小越好)。

其實,有一個神奇的數字14kb,它是怎麼來?

HTTP的傳輸層協議是TCP,TCP協議有一個慢啟動的過程,即它在第一次傳遞資料時,只能同時傳遞14kb的資料塊,所以當資料超多14kb時,TCP協議傳遞資料實際是多次的往返(roundtrip)。如果能夠將渲染所需要的資源控制在14kb之內,那麼就能TCP協議啟動時,一次完成資料的傳遞。

其他Web資源和關鍵渲染路徑的關係

你一定會思考,除了HTML,JavaScript和CSS,Web頁面還包含許多其他的資源,比如:圖片,網路字型(Icon Font),他們和關鍵渲染路徑的關係是什麼?

大家對圖片載入感受都應該大致一樣,它會在頁面載入過程中或完成後,逐步顯示,也就是說它不是阻塞渲染的資源,它的痛點主要在於質量和資源大小的權衡,以及請求數量帶來的效能消耗(雪碧圖)。

網路字型,在網路載入比較慢的情況下,使用者可能會感受到字型或者圖形的變化(Icon Font)。其實,瀏覽器在渲染樹構建完成之後,會指示需要哪些字型在網頁上渲染指定文字,然後分派字型請求,瀏覽器執行佈局並將內容繪製到螢幕上,如果字型尚不可用,瀏覽器可能不會渲染任何文字畫素,待字型可用之後,再繪製文字畫素,當然,不同瀏覽器之間實際行為有所差異,這裡不在贅述,請參考文章尾部的資料連結。

總結

優化關鍵渲染路徑的最終目的是優先顯示和使用者操作相關的內容,減少低優先順序資源對瀏覽器渲染的阻塞,從而儘早顯示使用者真正關心的關鍵內容。頁面效能就是使用者體驗的一個重要維度,嘗試用感性的思維去思考理性的程式碼,也許真的能受益不少。

參考資料:

  1. Google關鍵渲染路徑
  2. 《Web效能即使用者體驗》
  3. peter.sh/experiments…

原文釋出時間:2018-06-29
原文作者:朱本威
本文來源掘金如需轉載請緊急聯絡作者


相關文章