關鍵渲染路徑

本人學習使用,侵權刪php

編者按:本文做者Berwin,W3C性能工做組成員,360導航高級前端工程師。《深刻淺出Vue.js》(正在出版)做者。css

 

一般咱們只須要編寫HTML,CSS,JavaScript屏幕上就會顯示出漂亮的頁面,但瀏覽器是如何使用咱們的代碼在屏幕上渲染像素的呢?html

瀏覽器將HTML,CSS,JavaScript轉換爲屏幕上所呈現的實際像素,這期間所經歷的一系列步驟,叫作關鍵渲染路徑(Critical Rendering Path)。前端

 

(圖1-1 關鍵渲染路徑的具體步驟)瀏覽器

圖1-1給出了關鍵渲染路徑的具體步驟。如圖所示,首先,瀏覽器獲取HTML並開始構建DOM(文檔對象模型 - Document Object Model)。而後獲取CSS並構建CSSOM(CSS對象模型 - CSS Object Model)。而後將DOM與CSSOM結合,建立渲染樹(Render Tree)。而後找到全部內容都處於網頁的哪一個位置,也就是佈局(Layout)這一步。最後,瀏覽器開始在屏幕上繪製像素。網絡

正常狀況下瀏覽器會以上面咱們描述的步驟進行渲染,但有一個特殊狀況是在構建DOM時碰見了JavaScript,這時狀況就會變得不太同樣。JavaScript會影響渲染的流程,因此它是性能領域很重要的部分,這個特殊狀況咱們後面再詳細討論,咱們先討論如何構建DOM和CSSOM。前端工程師

1. 構建DOM

瀏覽器會遵照一套定義完善的步驟來處理HTML並構建DOM。宏觀上,能夠分爲幾個步驟。如圖1-2所示。佈局

 

(圖1-2 構建DOM的具體步驟)性能

第一步(轉換):瀏覽器從磁盤或網絡讀取HTML的原始字節,並根據文件的指定編碼(例如 UTF-8)將它們轉換成字符,如圖1-3所示。學習

 

(圖1-3 將字節碼轉換成字符)

第二步(Token化):將字符串轉換成Token,例如:「<html>」、「<body>」等。Token中會標識出當前Token是「開始標籤」或是「結束標籤」亦或是「文本」等信息。

 

(圖1-4將字符串轉換成Token)

這時候你必定會有疑問,節點與節點之間的關係如何維護?

事實上,這就是Token要標識「起始標籤」和「結束標籤」等標識的做用。例如「title」Token的起始標籤和結束標籤之間的節點確定是屬於「title」的子節點。如圖1-5所示。

 

(圖1-5 節點之間的關係)

圖1-5給出了節點之間的關係,例如:「Hello」Token位於「title」開始標籤與「title」結束標籤之間,代表「Hello」Token是「title」Token的子節點。同理「title」Token是「head」Token的子節點。

第三步(生成節點對象並構建DOM):事實上,構建DOM的過程當中,不是等全部Token都轉換完成後再去生成節點對象,而是一邊生成Token一邊消耗Token來生成節點對象。換句話說,每一個Token被生成後,會馬上消耗這個Token建立出節點對象。

帶有結束標籤標識的Token不會建立節點對象

節點對象包含了這個節點的全部屬性。例如<img src="xxx.png" />標籤最終生成出的節點對象中會保存圖片地址等信息。

隨後經過「開始標籤」與「結束標籤」來識別並關聯節點之間的關係。最終,當全部Token都生成並消耗完畢後,咱們就獲得了一顆完整的DOM樹。從Token生成DOM的過程,如圖1-6所示。

 

(圖1-6 構建DOM)

圖1-6中每個虛線上有一個小數字,表示構建DOM的具體步驟。能夠看出,首先生成出htmlToken,並消耗Token建立出html節點對象。而後生成headToken並消耗Token建立出head節點對象,並將它關聯到html節點對象的子節點中。隨後生成titleToken並消耗Token建立出title節點對象並將它關聯到head節點對象的子節點中。最後生成bodyToken並消耗Token建立body節點對象並將它關聯到html的子節點中。當全部Token都消耗完畢後,咱們就獲得了一顆完整的DOM樹。

構建DOM的具體實現,與Vue的模板編譯原理很是類似,若想了解構建DOM的過程如何用代碼實現,能夠查看我以前寫的一篇關於Vue模板編譯原理的文章。也能夠期待一下個人新書,書裏面對Vue模板編譯原理講的比文章更細緻與透徹。

2. 構建CSSOM

DOM會捕獲頁面的內容,但瀏覽器還須要知道頁面如何展現。因此須要構建CSSOM(CSS對象模型 - CSS Object Model)。

構建CSSOM的過程與構建DOM的過程很是類似,當瀏覽器接收到一段CSS,瀏覽器首先要作的是識別出Token,而後構建節點並生成CSSOM。如圖2-1所示。

 

(圖2-1 構建CSSOM的具體過程)

假設瀏覽器接收到了下面這樣一段CSS:

body {font-size: 16px;}

 

p {color: red;}

 

p span {display:none;}

 

span {font-size: 14px;}

 

img {float: right;}

上面這段CSS最終通過一系列步驟後生成的CSSOM,如圖2-2所示。

 

(圖2-2 構建CSSOM的過程)

從圖中還能夠看出,body節點的子節點繼承了body的樣式規則(16px的字號)。這就是層疊規則以及CSS爲何叫CSS(層疊樣式表)。

這裏我要講一句題外話,HTML能夠逐步解析,它不須要等待全部DOM都構建完畢後再去構建CSSOM,而是在解析HTML構建DOM時,若碰見CSS會馬上構建CSSOM,它們能夠同時進行。但CSS不行,不完整的CSS是沒法使用的,由於CSS的每一個屬性均可以改變CSSOM,因此會存在這樣一個問題:假設前面幾個字節的CSS將字體大小設置爲16px,後面又將字體大小設置爲14px,那麼若是不把整個CSSOM構建完整,最終獲得的CSSOM實際上是不許確的。因此必須等CSSOM構建完畢才能進入到下一個階段,哪怕DOM已經構建完,它也得等CSSOM,而後才能進入下一個階段。

因此,CSS的加載速度與構建CSSOM的速度將直接影響首屏渲染速度,所以在默認狀況下CSS被視爲阻塞渲染的資源。

3. 構建渲染樹

DOM包含了頁面的全部內容,CSSOM包含了頁面的全部樣式,如今咱們須要將DOM和CSSOM組成渲染樹。

假設咱們如今有這樣一段代碼:

<!doctype html>

 

<html>

 

<head>

 

 <meta charset="UTF-8">

 

 <title>Demos</title>

 

 <style>

 

 body {font-size: 16px;}

 

 p {color: red;}

 

 p span {display:none;}

 

 span {font-size: 14px;}

 

 img {float: right;}

 

 </style>

 

</head>

 

<body>

 

 <p>Hello <span>berwin</span></p>

 

 <span>Berwin</span>

 

 <img src="https://p1.ssl.qhimg.com/t0195d63bab739ec084.png" />

 

</body>

 

</html>

這段代碼最終構建成渲染樹,如圖3-1所示。

 

(圖3-1 構建渲染樹)

渲染樹的重要特性是它僅捕獲可見內容,構建渲染樹瀏覽器須要作如下工做:

  1. 從 DOM 樹的根節點開始遍歷每一個可見節點。

  2. 有些節點不可見(例如腳本Token、元Token等),由於它們不會體如今渲染輸出中,因此會被忽略。

  3. 某些節點被CSS隱藏,所以在渲染樹中也會被忽略。例如:上圖中的p > span節點就不會出如今渲染樹中,由於該節點上設置了display: none屬性。

  4. 對於每一個可見節點,爲其找到適配的 CSSOM 規則並應用它們。

因此最終渲染出的結果以下圖所示。

 

(圖3-2 渲染樹與渲染結果)

4. 佈局

有了渲染樹以後,接下來進入佈局階段。這一階段瀏覽器要作的事情是要弄清楚各個節點在頁面中的確切位置和大小。一般這一行爲也被稱爲「自動重排」。

佈局流程的輸出是一個「盒模型」,它會精確地捕獲每一個元素在視口內的確切位置和尺寸,全部相對測量值都將轉換爲屏幕上的絕對像素。如圖4-1所示。

 

(圖4-1 佈局)

5. 繪製

佈局完成後,瀏覽器會當即發出「Paint Setup」和「Paint」事件,將渲染樹轉換成屏幕上的像素。如圖5-1所示。

 

(圖5-1 繪製)

6. JS與關鍵渲染路徑

如今,咱們回到文章的最開始時留下的問題,咱們討論關鍵渲染路徑,可是以前的討論並不包含JS。這是由於JS會打破前面咱們討論的內容。

咱們都知道JavaScript的加載、解析與執行會阻塞DOM的構建,也就是說,在構建DOM時,HTML解析器若遇到了JavaScript,那麼它會暫停構建DOM,將控制權移交給JavaScript引擎,等JavaScript引擎運行完畢,瀏覽器再從中斷的地方恢復DOM構建。

由於JavaScript能夠修改網頁的內容,它能夠更改DOM,若是不阻塞,那麼這邊在構建DOM,那邊JavaScript在改DOM,如何保障最終獲得的DOM是否正確?並且在JS中前一秒獲取到的DOM和後一秒獲取到的DOM不同是什麼鬼?它會產生一系列問題,因此JS是阻塞的,它會阻塞DOM的構建流程,因此在JS中沒法獲取JS後面的元素,由於DOM還沒構建到那。

JavaScript對關鍵渲染路徑的影響不僅是阻塞DOM的構建,它會致使CSSOM也阻塞DOM的構建。

本來DOM和CSSOM的構建是互不影響,井水不犯河水,可是一旦引入了JavaScript,CSSOM也開始阻塞DOM的構建,只有CSSOM構建完畢後,DOM再恢復DOM構建。

這是什麼狀況?

這是由於JavaScript不僅是能夠改DOM,它還能夠更改樣式,也就是它能夠更改CSSOM。前面咱們介紹,不完整的CSSOM是沒法使用的,但JavaScript中想訪問CSSOM並更改它,那麼在執行JavaScript時,必需要能拿到完整的CSSOM。因此就致使了一個現象,若是瀏覽器還沒有完成CSSOM的下載和構建,而咱們卻想在此時運行腳本,那麼瀏覽器將延遲腳本執行和DOM構建,直至其完成CSSOM的下載和構建。

也就是說,在這種狀況下,瀏覽器會先下載和構建CSSOM,而後再執行JavaScript,最後在繼續構建DOM。

這會致使嚴重的性能問題,咱們假設構建DOM須要一秒,構建CSSOM須要一秒,那麼正常狀況下只須要一秒鐘DOM和CSSOM就會同時構建完畢而後進入到下一個階段。可是若是引入了JavaScript,那麼JavaScript會阻塞DOM的構建並等待CSSOM的下載和構建,一秒鐘以後,假設執行JavaScript須要0.00000001秒,那麼從中斷的地方恢復DOM的構建後,還須要一秒鐘的時間才能完成DOM的構建,總共花費了2秒鐘才進入到下一個階段。如圖6-1所示。

 

(圖6-1 JS阻塞構建DOM並等待CSSOM)

例以下面不加載JS的代碼:

<!doctype html>

 

<html>

 

<head>

 

 <meta charset="UTF-8">

 

 <title>Test</title>

 

 <link rel="stylesheet" href="https://static.xx.fbcdn.net/rsrc.php/v3/y6/l/1,cross/9Ia-Y9BtgQu.css">

 

</head>

 

<body>

 

 aa

 

</body>

 

</html>

上面這段代碼的執行性能結果,如圖6-2所示。

 

(圖6-2 CSS不阻塞DOM)

DOMContentLoaded 事件在116ms左右觸發。

在代碼中添加JavaScript:

<!doctype html>

 

<html>

 

<head>

 

 <meta charset="UTF-8">

 

 <title>Test</title>

 

 <link rel="stylesheet" href="https://static.xx.fbcdn.net/rsrc.php/v3/y6/l/1,cross/9Ia-Y9BtgQu.css">

 

</head>

 

<body>

 

 aa

 

 <script>

 

 console.log(1)

 

 </script>

 

</body>

 

</html>

DOMContentLoaded 事件在1.21s觸發,如圖6-3所示。

 

(圖6-3 CSS阻塞DOM)

7. 總結

關鍵渲染路徑(Critical Rendering Path)是指瀏覽器將HTML,CSS,JavaScript轉換爲屏幕上所呈現的實際像素這期間所經歷的一系列步驟。

關鍵渲染路徑共分五個步驟。構建DOM -> 構建CSSOM -> 構建渲染樹 -> 佈局 -> 繪製。

CSSOM會阻塞渲染,只有當CSSOM構建完畢後纔會進入下一個階段構建渲染樹。

一般狀況下DOM和CSSOM是並行構建的,可是當瀏覽器遇到一個script標籤時,DOM構建將暫停,直至腳本完成執行。但因爲JavaScript能夠修改CSSOM,因此須要等CSSOM構建完畢後再執行JS。

相關文章
相關標籤/搜索