做爲Web工程師,咱們天天寫HTML,CSS和JavaScript,可是瀏覽器是如何解析這些文件,最終將它們以像素顯示在屏幕上的呢?javascript
這一過程叫作Critical Rendering Path。css
Critical Rendering Path,中文翻譯過來,叫作關鍵渲染路徑。指的是瀏覽器從請求HTML,CSS,JavaScript文件開始,到將它們最終以像素輸出到屏幕上這一過程。包括如下幾個部分:html
值得注意的是,上面的過程並非依次進行的,而是存在必定交叉,後面會詳細解釋。java
想要提升網頁加載速度,提高用戶體驗,就須要在第一次加載時讓重要的元素儘快顯示在屏幕上。而不是等全部元素所有準備就緒再顯示,下面一幅圖說明了這兩種方式的差別。git
DOM (Document Object Model),文檔對象模型,構建DOM是必不可少的一環,瀏覽器從發出請求開始到獲得HTML文件後,第一件事就是解析HTML成許多Tokens,再將Tokens轉換成object,最後將object組合成一顆DOM樹。github
這個過程是一個按部就班的過程,咱們假設HTML文件很大,一個*RTT (Round-Trip Time)*只能獲得一部分,瀏覽器獲得這部分以後就會開始構建DOM,並不會等到整個文檔就位纔開始渲染。這樣作能夠加快構建過程,並且因爲自頂向下構建,所以後面構建的不會對前面的形成影響。web
後面咱們將會提到,CSSOM則必須等到全部字節收到纔開始構建。瀏覽器
CSSOM (CSS Object Model),CSS對象模型,構建過程相似DOM,當HTML解析中遇到<link>
標籤時,會請求對應的CSS文件,當CSS文件就位時,便開始解析它(若是遇到行內<style>
時則直接解析),這一解析過程能夠和構建DOM同時進行。緩存
假設有以下CSS代碼網絡
body { font-size: 16px }
p { font-weight: bold }
span { color: red }
p span { display: none }
img { float: right }
複製代碼
構建出來的CSSOM是這樣的:
須要注意的是,上面並非一顆完整的CSSOM樹,文檔有一些默認的CSS樣式,稱做user agent styles,上面只展現了咱們覆蓋的部分。
CSSOM的構建必需要得到一份完整的CSS文件,而不像DOM的構建是一個按部就班的過程。由於咱們知道,CSS文件中包含大量的樣式,後面的樣式會覆蓋前面的樣式,若是咱們提早就構建CSSOM,可能會獲得錯誤的結果。
這也是關鍵的一步,瀏覽器使用DOM和CSSOM構建出Render Tree。此時不像構建DOM同樣把全部節點構建出來,瀏覽器只構建須要在屏幕上顯示的部分,所以像<head>
,<meta>
這些標籤就無需構建了。同時,對於display: none
的元素,也無需構建。
display: none
告訴瀏覽器這個元素無需出如今Render Tree中,可是visibility: hidden
只是隱藏了這個元素,可是元素還佔空間,會影響到後面的Layout,所以仍然須要出如今Render Tree中。
構建過程遵循如下步驟
相信大多數初學者都會認爲CSS匹配是左向右的,其實偏偏相反。學習了CRP,也就不難理解爲何了。
CSS匹配就發生在Render Tree構建時(Chrome Dev Tools裏叫作Recalculate Style),此時瀏覽器構建出了DOM,並且拿到了CSS樣式,此時要作的就是把樣式跟DOM上的節點對應上,瀏覽器爲了提升性能須要作的就是快速匹配。
首先要明確一點,瀏覽器此時是給一個"可見"節點找對應的規則,這和jQuery選擇器不一樣,後者是使用一個規則去找對應的節點,這樣從左到右或許更快。可是對於前者,因爲CSS的龐大,一個CSS文件中或許有上千條規則,並且對於當前節點來講,大多數規則是匹配不上的,到此爲止,稍微想一下就知道,若是從右開始匹配(也是從更精確的位置開始),能更快排除不合適的大部分節點,而若是從左開始,只有深刻了纔會發現匹配失敗,若是大部分規則層級都比較深,就比較浪費資源了。
除了上面這點,咱們前面還提到DOM構建是"按部就班的",並且DOM不阻塞Render Tree構建(只有CSSOM阻塞),這樣也是爲了能讓頁面更早有元素呈現。考慮以下狀況,若是咱們此時構建的只是部分DOM,而此時CSSOM構建完成,瀏覽器此時須要構建Render Tree,若是對每個節點,找到一條規則進行從左向右匹配,則必需要求其子元素甚至孫子元素都在DOM上(而此時DOM未構建完成),顯然會匹配失敗。若是反過來,咱們只須要查找該元素的父元素或祖先元素(它們確定在當前DOM中)。
咱們如今爲止已經獲得了全部元素的自身信息,可是還不知道它們相對於Viewport的位置和大小,Layout這一過程須要計算的就是這兩個信息。
根據這兩個信息,Layout輸出元素的Box Model,關於這個,我也寫過一篇文章Understand CSS Formatting Model。
目前爲止,如今咱們已經拿到了元素相對於Viewport的詳細信息,全部的值都已經計算爲相對Viewport的精確像素大小和位置,就差顯示了。
瀏覽器將每個節點以像素顯示在屏幕上,最終咱們看到頁面。
這一過程須要的時間與文檔大小,CSS應用樣式的多少以及複雜度,還有設備自身都有關,例如對簡單的顏色進行Paint是簡單的,可是box-shadow
進行paint則是複雜的
前面的過程都沒有提到JavaScript,但在現在,JavaScript倒是網頁中不可缺的一部分。這裏對它如何影響CRP作一個概要,具體細節我後面使用Chrome Dev Tools進行了測驗
<style>
和<link>
的狀況,若是放在尾部,瀏覽器剛開始會使用User Agent Style構建CSSOM)爲了模擬真實網絡狀況,我把Demo部署到了個人githubpage,你也能夠在倉庫找到源代碼。
同時,不要混淆DOM, CSSOM, Render Tree這三個概念,我剛開始就容易混淆DOM和Render Tree,這兩個是不一樣的
下面的Chrome截圖部分,若是不清晰,請直接點擊圖片查看原圖
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="../css/main.css" />
<title>Critical Rendering Path with separate script file</title>
</head>
<body>
<p>What's up? <span>Bros. </span>My name is tianzhich</p>
<div><img src="../images/xiaoshuang.jpg" alt="小爽-流星雨" height="500"></div>
<script src="../js/main.js"></script>
</body>
</html>
複製代碼
var span = document.getElementsByTagName('span')[0];
span.textContent = 'Girls. '; // change DOM text content
span.style.display = 'inline'; // change CSSOM property
// create a new element, style it, and append it to the DOM
var loadTime = document.createElement('div');
loadTime.textContent = 'You loaded this page on: ' + new Date();
loadTime.style.color = 'blue';
document.body.appendChild(loadTime);
複製代碼
/* // [START full] */
body {
font-size: 16px
}
p {
font-weight: bold
}
span {
color: red
}
p span {
display: none
}
img {
float: right
}
/* // [END full] */
複製代碼
首先來看沒有加載JS的狀況
上圖中,瀏覽器收到HTML文件後,便開始解析構建DOM。
須要注意,上圖接收的只是圖片的一部分
接下來咱們詳細看看這三個部分:
能夠看出,瀏覽器解析到<link>
,<img>
等等標籤時,會立刻發出HTTP請求,並且解析也將繼續進行,解析完成後會觸發readystatechange事件和DOMContentLoaded事件,在上圖中,因爲時間間隔已經到了100微秒級別,事件間隔有些許差別,但不影響咱們對這一過程的理解
細心的話可能會注意到上圖中還觸發了Recalculate Style (紫色部分),這一過程發生在CSSOM樹構建完成或者發生變化須要更新Render Tree時,可是此時咱們並無拿到CSS,更沒有構建出CSSOM,這一部分從何而來呢?我在下面第4部分作了分析
下面這一過程依次展現了CSS解析構建CSSOM,Render Tree生成,layout和paint,最終頁面首次出現畫面
下圖中有一點錯誤:render tree構建應該發生在Recalculate Style (layout前一部分),Layout以及後一部分Update Layer Tree做爲Layout
從這裏咱們能夠看出,DOM即便構建完成,也須要等CSSOM構建完成,才能通過一個完整的CRP並呈現畫面,所以爲了畫面儘快呈現,咱們須要儘早構建出CSSOM,好比
<style>
或者<link>
標籤應該放在<head>
裏並儘早發現被解析(第4部分我會分析將這兩個標籤放在html文檔後面形成的影響)<style>
標籤發在<head>
裏,無需網絡請求上圖說明,瀏覽器接收到部分圖片字節後,便開始渲染了,而不是等整張圖片接收完成纔開始渲染,至於渲染次數,本例中的圖片大小爲90KB左右,傳輸了6次,渲染了2次。我以爲這應該和網絡擁塞程度以及圖片大小等因素有關。
還有一點須要注意,兩次渲染中,只有首次渲染引起了Layout和以後的Update Layer Tree,而第二次渲染只有Update Layer Tree,我猜測對於圖片來講,瀏覽器第一次渲染便知道了其大小,因此從新進行Layout並留出足夠空間,以後的渲染只須要在該空間上進行paint便可。整張圖片加載完畢以後,觸發Load事件
上圖包括以後圖片中的Chrome擴展腳本能夠忽視,雖然使用了隱私模式作測驗(避免緩存和一些擴展腳本的影響),但我發現仍是有一個腳本沒法去除,雖然這不影響測驗結果
接下來咱們考慮JavaScript腳本對CRP的影響
上圖來看,Parse HTML這一過程被JavaScript執行打斷,並且JavaScript會等待CSSOM構建完成以後再執行,執行完成以後,DOM繼續構建
前面的例子中,咱們看到DOM幾乎都是在CSSOM構建完成前就構建完成了,而引入JS後,DOM構建被JS執行打斷,而JS執行又必須等CSSOM構建完畢,這無疑延長了第一次CRP時間,讓頁面首次出現畫面的時間更長
若是使用外部script腳本,這一時間會更長
對於網絡請求的資源,瀏覽器會爲其分配優先級,優先級越高的資源響應速度更快,時間更短,在這個例子中,CSS的優先級最高,其次是JS,優先級最低的是圖片
咱們主要來看第一部分,後面部分和第1個研究相似
能夠看到,增長了對JS文件的網絡請求時間,一輪CRP時間更長了,對比上面的行內Script可能時間差別沒有那麼明顯,是由於這個例子中的JS文件體積小,傳輸時間只比CSS多一點,主要決定JS什麼時候執行的仍是CSS,若是JS稍大,因爲請求優先級低於CSS,則差別會明顯變大
若是Script會對頁面首次渲染形成這麼大的影響,有沒有什麼好的辦法解決它呢?
答案是確定的,就是使用異步腳本<script src="" async />
使用異步腳本,其實就是告訴瀏覽器幾件事
須要注意以下幾點
async
關鍵字變成異步,並且注意其與延遲腳本 (<script defer>
)的區別,後者是在Document被解析完畢而DOMContentLoaded事件觸發以前執行,前者則是在下載完畢後執行document.createElement
建立的<script>
,默認就是異步腳本直接看圖
因爲Script執行修改了DOM和CSSOM,所以從新通過Recalculate Style生成Render Tree,從新計算Layout,從新Paint,最終呈現頁面。因爲這一過程仍然很快(只用了140ms左右),所以咱們仍是察覺不到這個變化
前面留下了一個問題,CSSOM沒有構建完成,爲何剛開始的Parse HTML同時就有Recalculate Style這部分?或許這部分會給你一個答案
這裏爲了不JS帶來的影響,使對比更有針對性,刪除了JavaScript
先來回顧一下在頭部設置<link>
前面的DOM構建部分出現了Recalculate Style,以後得到CSS並解析後還有一次,一共出現了2次
再來看看改爲<style>
,Recalculate Style一共出現1次
<style>
在頭部,一開始就直接解析完成,沒有網絡請求
先來看看設置<style>
在尾部,Recalculate Style出現了1次
再看設置<link>
在尾部,Recalculate Style一共出現3次
先總結實驗結果
實驗中將<link>
放在頭部,<style>
放在頭部,<link>
放在尾部,<style>
放在尾部,Recalculate Style的次數分別是2,1,3,1
而後咱們須要瞭解Chrome Dev Tools Performance Tab的幾個關鍵過程
To find out how long the CSS processing takes you can record a timeline in DevTools and look for "Recalculate Style" event: unlike DOM parsing, the timeline doesn’t show a separate "Parse CSS" entry, and instead captures parsing and CSSOM tree construction, plus the recursive calculation of computed styles under this one event.
在Performance Tab裏面,沒有看到Render Tree構建這一過程,這一過程也被瀏覽器隱藏在Recalculate Style裏面,因此Recalculate Style既可能包括CSSOM的構建,也可能包括Render Tree的構建
對於<style>
裏的CSS,解析過程發生在Recalculate Style中,而<link>
得到的CSS,解析過程是單獨的,叫作Parse CSS (和Parse HTML相似)
同時,要明確瀏覽器還有一個默認的User Agent Style,咱們的Style只是對其進行一個覆蓋
最後猜測這4個結果的緣由以下
<head>
裏存在<link>
,則會等待CSS網絡請求完成並解析好以後纔開始Render Tree,至於第一次的Recalculate Style,我猜測是默認的User Agent Style,此時CSSOM已經開始構建了,而接收到CSS文件,咱們設置的Style會對默認的Style進行覆蓋。這裏第一次Recalculate Style只包含CSSOM構建,第二次則包含了CSSOM更新以及Render Tree構建<style>
放在頭部,瀏覽器由於能夠立刻拿到CSS,就能夠立刻進行解析,此時User Agent Style的解析和咱們自定義的Style解析合併,Recalculate Style包含了CSSOM構建和Render Tree構建<style>
放在尾部,和放在頭部相似。只不過晚點發現CSS,可是因爲是行內<style>
,仍是能夠立刻解析<link>
放在尾部,瀏覽器一開始沒發現<link>
,會使用User Agent Style(2次 Recalculate Style),後面才發CSS網絡請求,最後再觸發CSSOM的更新(1次 Recalculate Style),這是最糟糕的狀況。這裏的3次Recalculate Style分別指CSSOM構建,Render Tree構建,CSSOM更新和Render Tree構建。Render Tree構建兩次,頁面發生兩次渲染,爲最糟糕的狀況因此,咱們須要將<style>
和<link>
放在頭部,對於<style>
在尾部,這個例子省略了JS的影響,若是加入JS,則結果又會不同
原本想再測試一下JS在HTML中不一樣位置的影響,可是就CRP這一過程來說,這部分比較容易敘述清楚
由於JS無論在哪一個位置都會默認阻塞DOM。若是DOM還沒有構建完成,JS中對不在DOM的元素進行操做,會拋出錯誤,腳本直接結束。若是將腳本設置爲async
,則放在前面也是OK的,例如使用document.createElement
建立的<script>
,其默認使用的就是異步
這篇文章是我閱讀了Google Developer的Web Performance Fundamentals後,本身作實踐獲得的總結。很是建議每位FEDers閱讀這一系列文章。文章做者Ilya Grigorik還和Udacity開設了聯合課程Website Performance Optimization以及他關於Web Performance的一次演講,都值得一看。
因爲水平有限,我只看了前半部分(關於CRP),後半部分則關於在Web Performance Optimization的實踐。
疏漏之處,歡迎指正。