網頁呈現關鍵渲染路徑

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

瀏覽器將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,例如:“”、“

”等。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不會創建節點對象

節點對象包含了這個節點的所有屬性。例如 標籤最終生成出的節點對象中會保存圖片地址等信息。

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

網頁呈現關鍵渲染路徑

圖1-6 構建DOM

圖1-6中每一個虛線上有一個小數字,表示構建DOM的具體步驟。可以看出,首先生成出 html Token,並消耗Token創建出 html 節點對象。然後生成 head Token並消耗Token創建出 head 節點對象,並將它關聯到 html 節點對象的子節點中。隨後生成 title Token並消耗Token創建出 title 節點對象並將它關聯到 head 節點對象的子節點中。最後生成 body Token並消耗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組成渲染樹。

假設我們現在有這樣一段代碼:





<title>Demos/<title>



Hello berwin


Berwin



複製代碼

這段代碼最終構建成渲染樹,如圖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的代碼:





<title>Test/<title>
<link>


aa


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

網頁呈現關鍵渲染路徑

圖6-2 CSS不阻塞DOM

DOMContentLoaded 事件在116ms左右觸發。

在代碼中添加JavaScript:





<title>Test/<title>
<link>


aa



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

網頁呈現關鍵渲染路徑

圖6-3 CSS阻塞DOM

7. 總結

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

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

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

通常情況下DOM和CSSOM是並行構建的,但是當瀏覽器遇到一個>


分享到:


相關文章: