關鍵渲染路徑
通常我們只需要編寫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,例如:“<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的具體步驟。可以看出,首先生成出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組成渲染樹。
假設我們現在有這樣一段程式碼:
<!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 構建渲染樹渲染樹的重要特性是它僅捕獲可見內容,構建渲染樹瀏覽器需要做以下工作:
- 從 DOM 樹的根節點開始遍歷每個可見節點。
- 有些節點不可見(例如指令碼Token、元Token等),因為它們不會體現在渲染輸出中,所以會被忽略。
- 某些節點被CSS隱藏,因此在渲染樹中也會被忽略。例如:上圖中的
p > span
節點就不會出現在渲染樹中,因為該節點上設定了display: none
屬性。
- 對於每個可見節點,為其找到適配的 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所示。
例如下面不載入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不阻塞DOMDOMContentLoaded 事件在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阻塞DOM7. 總結
關鍵渲染路徑(Critical Rendering Path)是指瀏覽器將HTML,CSS,JavaScript轉換為螢幕上所呈現的實際畫素這期間所經歷的一系列步驟。
關鍵渲染路徑共分五個步驟。構建DOM -> 構建CSSOM -> 構建渲染樹 -> 佈局 -> 繪製。
CSSOM會阻塞渲染,只有當CSSOM構建完畢後才會進入下一個階段構建渲染樹。
通常情況下DOM和CSSOM是並行構建的,但是當瀏覽器遇到一個script
標籤時,DOM構建將暫停,直至指令碼完成執行。但由於JavaScript可以修改CSSOM,所以需要等CSSOM構建完畢後再執行JS。
我的部落格地址:github.com/berwin/Blog…