優化關鍵渲染路徑

Berwin發表於2019-01-21

上個月,我寫了一篇文章介紹什麼是“關鍵渲染路徑”,其實目的是為了給這篇文章做一個鋪墊,本文將談談如何優化關鍵渲染路徑(本文將假設您已經閱讀過《關鍵渲染路徑》這篇文章或已經懂得了什麼是“關鍵渲染路徑”)。

優化關鍵渲染路徑可以提升網頁的渲染速度,從而得到一個更好的使用者體驗。

如何優化關鍵渲染路徑?

優化關鍵渲染路徑有很多種方法與情況,不同情況下優化方式也各不相同,初步看起來這些優化方法五花八門,知識非常的零散。

但在這些看似零散的知識中,我們會發現一些規律,將這些規律總結起來後,可以得出一個結論:到目前為止,只有三種因素可以影響關鍵渲染路徑的耗時。而所有的優化方式,都是在儘可能的針對這三種因素進行優化。

這三種因素分別是:

  • 關鍵資源的數量
  • 關鍵路徑的長度
  • 關鍵位元組的數量

切記,非常重要,所有優化關鍵渲染路徑的方法,都是在優化以上三種因素。因為只有這三種因素可以影響關鍵渲染路徑。

關鍵資源指的是那些可以阻塞頁面首次渲染的資源。例如JavaScript、CSS都是可以阻塞關鍵渲染路徑的資源,這些資源就屬於“關鍵資源”。關鍵資源的數量越少,瀏覽器處理渲染的工作量就越少,同時CPU及其他資源的佔用也越少。

關鍵路徑中的每一步耗時越長,由於阻塞會導致渲染路徑的整體耗時變長。關鍵路徑的長度指的是關鍵渲染路徑的總耗時。關鍵渲染路徑的長度會受到很多因素的影響。例如:關鍵資源的網路情況、關鍵資源的數量、關鍵資源的位元組大小、關鍵資源的依賴關係等。

關鍵位元組的數量指的是關鍵資源的位元組大小,瀏覽器要下載的資源位元組越小,則下載速度與處理資源的速度都會更快。通常很多優化方法都是針對關鍵位元組的數量進行優化。例如:壓縮。

優化DOM

在關鍵渲染路徑中,構建渲染樹(Render Tree)的第一步是構建DOM,所以我們先討論如何讓構建DOM的速度變得更快。

HTML檔案的尺寸應該儘可能的小,目的是為了讓客戶端儘可能早的接收到完整的HTML。通常HTML中有很多冗餘的字元,例如:JS註釋、CSS註釋、HTML註釋、空格、換行。更糟糕的情況是我見過很多生產環境中的HTML裡面包含了很多廢棄程式碼,這可能是因為隨著時間的推移,專案越來越大,由於種種原因從歷史遺留下來的問題,不過不管怎麼說,這都是很糟糕的。對於生產環境的HTML來說,應該刪除一切無用的程式碼,儘可能保證HTML檔案精簡。

總結起來有三種方式可以優化HTML:縮小檔案的尺寸(Minify)使用gzip壓縮(Compress)使用快取(HTTP Cache)

縮小檔案的尺寸(Minify)會刪除註釋、空格與換行等無用的文字。

本質上,優化DOM其實是在儘可能的減小關鍵路徑的長度與關鍵位元組的數量

優化CSSOM

與優化DOM類似,CSS檔案也需要讓檔案儘可能的小,或者說所有文字資源都需要。CSS檔案應該刪除未使用的樣式、縮小檔案的尺寸(Minify)、使用gzip壓縮(Compress)、使用快取(HTTP Cache)。

除了上面提到的優化策略,CSS還有一個可以影響效能的因素是:CSS會阻塞關鍵渲染路徑

CSS是關鍵資源,它會阻塞關鍵渲染路徑也並不奇怪,但通常並不是所有的CSS資源都那麼的『關鍵』。

舉個例子:一些響應式CSS只在螢幕寬度符合條件時才會生效,還有一些CSS只在列印頁面時才生效。這些CSS在不符合條件時,是不會生效的,所以我們為什麼要讓瀏覽器等待我們並不需要的CSS資源呢?

針對這種情況,我們應該讓這些非關鍵的CSS資源不阻塞渲染

實現這一目的非常簡單,我們只需要將不阻塞渲染的CSS移動到單獨的檔案裡。例如我們將列印相關的CSS移動到print.css,然後我們在HTML中引入CSS時,新增媒體查詢屬性print,程式碼如下:

<link href="print.css" rel="stylesheet" media="print">
複製程式碼

上面程式碼新增了media="print"屬性,所以上面CSS資源僅用於列印。新增了媒體查詢屬性後,瀏覽器依然會下載該資源,但如果條件不符合,那麼它就不再阻塞渲染,也就是變成了非阻塞的CSS

我們可以寫個DEMO測試一下:

<!doctype html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Demos</title>
    <link rel="stylesheet" href="https://static.xx.fbcdn.net/rsrc.php/v3/y6/l/1,cross/9Ia-Y9BtgQu.css">
</head>
<body>
    Hello
</body>
</html>
複製程式碼

上面程式碼使用Chrome開發者工具的效能皮膚捕獲後的效能圖如下:

阻塞的CSS資源效能捕獲圖

從上圖中的首次繪製(First Paint)時間是在1200ms的位置,可以看到這個時間是瀏覽器載入CSS完畢後,而且可以看到Network欄中CSS顯示Highest代表高優先順序。

新增了媒體查詢語句後,捕獲出來的效能圖如下:

非阻塞的CSS資源效能捕獲圖

首次繪製時間在不到100ms的位置,和domcontentloaded事件差不多的時間觸發。同時CSS資源變成了Lowest,表示低優先順序。

可以看到,瀏覽器依然會下載該CSS資源,但它不再阻塞渲染。

上面提供的方法是針對那些不需要生效的CSS資源,如果CSS資源需要在當前頁面生效,只是不需要在首屏渲染時生效,那麼為了更快的首屏渲染速度,我們可以將這些CSS也設定成非關鍵資源。只是我們需要一些比較hack的方式來實現這個需求:

<link href="style.css" rel="stylesheet" media="print" onload="this.media='all'">
複製程式碼

上面程式碼先把媒體查詢屬性設定成print,將這個資源設定成非阻塞的資源。然後等這個資源載入完畢後再將媒體查詢屬性設定成all讓它立即對當前頁面生效。

通過這樣的方式,我們既可以讓這個資源不阻塞關鍵渲染路徑,還可以讓它載入完畢後對當前頁面生效。

類似的方案有很多,程式碼如下:

<link rel="preload" href="style.css" as="style" onload="this.rel='stylesheet'">

<link rel="alternate stylesheet" href="style.css" onload="this.rel='stylesheet'">
複製程式碼

上面兩種方式都能實現同樣的效果。

關於CSS的載入有這麼多門道,到底怎樣才是最佳實踐?答案是:Critical CSS

Critical CSS的意思是:把首屏渲染需要使用的CSS通過style標籤內嵌到head標籤中,其餘CSS資源使用非同步的方式非阻塞載入。

CSS資源在構建渲染樹時,會阻塞JavaScript,所以我們應該保證所有與首屏渲染無關的CSS資源都應該被標記為非關鍵資源。

所以Critical CSS從兩個方面解決了效能問題:

  1. 減少關鍵資源的數量(將所有與首屏渲染無關的CSS使用非同步非阻塞載入)
  2. 減少關鍵路徑的長度(將首屏渲染需要的CSS直接內嵌到head標籤中,移除了網路請求的時間)。

避免使用@import

大家應該都知道要避免使用@import載入CSS,實際工作中我們也不會這樣去載入CSS,但這到底是為什麼呢?

這是因為使用@import載入CSS會增加額外的關鍵路徑長度。舉個例子:

<!doctype html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Demos</title>
    <link rel="stylesheet" href="http://127.0.0.1:8887/style.css">
    <link rel="stylesheet" href="https://lib.baomitu.com/CSS-Mint/2.0.6/css-mint.min.css">
</head>
<body>
    <div class="cm-alert">Default alert</div>
</body>
</html>
複製程式碼

上面這段程式碼使用link標籤載入了兩個CSS資源。這兩個CSS資源是並行下載的。我們使用Chrome開發者工具的Performance皮膚捕獲出的結果如下圖所示:

使用link標籤載入樣式

從圖中用紅色方框圈出來的位置可以看出兩個CSS是並行載入的,首次繪製時間取決於CSS載入時間較長的資源載入時間。

現在我們改為使用@import載入資源,程式碼如下:

<!doctype html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Demos</title>
    <link rel="stylesheet" href="http://127.0.0.1:8887/style.css">
</head>
<body>
    <div class="cm-alert">Default alert</div>
</body>
</html>
複製程式碼
/* style.css */
@import url('https://lib.baomitu.com/CSS-Mint/2.0.6/css-mint.min.css');
body{background:red;}
複製程式碼

程式碼中使用link標籤載入一個CSS,然後在CSS檔案中使用@import載入另一個CSS。使用Chrome開發者工具再次捕獲出的結果如下圖所示:

使用@import載入CSS

可以看到兩個CSS變成了序列載入,前一個CSS載入完後再去下載使用@import匯入的CSS資源。這無疑會導致載入資源的總時間變長。從上圖可以看出,首次繪製時間等於兩個CSS資源載入時間的總和。

所以避免使用@import是為了降低關鍵路徑的長度。

非同步JavaScript

所有文字資源都應該讓檔案儘可能的小,JavaScript也不例外,它也需要刪除未使用的程式碼、縮小檔案的尺寸(Minify)、使用gzip壓縮(Compress)、使用快取(HTTP Cache)。

與CSS資源相似,JavaScript資源也是關鍵資源,JavaScript資源會阻塞DOM的構建。並且JavaScript會被CSS檔案所阻塞。為了避免阻塞,可以為script標籤新增async屬性。

我們舉個例子:

<!doctype html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Demos</title>
    <link rel="stylesheet" href="https://static.xx.fbcdn.net/rsrc.php/v3/y6/l/1,cross/9Ia-Y9BtgQu.css">
</head>
<body>
    <p class='_159h'>aa</p>
    <script src="http://qiniu.bkt.demos.so/static/js/app.53df42d5b7a0dbf52386.js"></script>
</body>
</html>
複製程式碼

上面這段程式碼,分別載入了CSS資源和JavaScript資源,我們使用Chrome開發者工具的Performance皮膚捕獲出的結果如下圖所示:

同步載入JS資源

從捕獲出的結果可以看到,JS資源載入完畢後,需要等待CSS資源載入完並構建出CSSOM之後才會執行JS,並且JS會將DOM阻塞,所以最終domcontentloaded事件在350ms與400ms之間觸發。

我們將script標籤新增async屬性:

<!doctype html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Demos</title>
    <link rel="stylesheet" href="https://static.xx.fbcdn.net/rsrc.php/v3/y6/l/1,cross/9Ia-Y9BtgQu.css">
</head>
<body>
    <p class='_159h'>aa</p>
    <script async src="http://qiniu.bkt.demos.so/static/js/app.53df42d5b7a0dbf52386.js"></script>
</body>
</html>
複製程式碼

使用Chrome開發者工具捕獲出的結果如下圖所示:

非同步載入JS

從圖中可以看到,JS載入完後不再需要等待CSS資源,並且也不再阻塞DOM的構建,最終domcontentloaded事件在50ms與100ms之間觸發。與之前相比,domcontentloaded事件觸發時間提前了300ms。

可以看到,在關鍵渲染路徑中優化JavaScript,目的是為了減少關鍵資源的數量

總結

該篇文章詳細介紹瞭如何優化關鍵渲染路徑。

關鍵渲染路徑是瀏覽器將HTML,CSS,JavaScript轉換為螢幕上所呈現的實際畫素的具體步驟。而優化關鍵渲染路徑可以提高網頁的呈現速度,也就是首屏渲染優化。

你會發現,我們介紹的內容都是如何優化DOM,CSSOM以及JavaScript,因為通常在關鍵渲染路徑中,這些步驟的效能最差。這些步驟是導致首屏渲染速度慢的主要原因。

相關文章