深刻理解瀏覽器解析渲染HTML

前言

做爲Web工程師,咱們天天寫HTML,CSS和JavaScript,可是瀏覽器是如何解析這些文件,最終將它們以像素顯示在屏幕上的呢?javascript

這一過程叫作Critical Rendering Pathcss

Critical Rendering Path

Critical Rendering Path,中文翻譯過來,叫作關鍵渲染路徑。指的是瀏覽器從請求HTML,CSS,JavaScript文件開始,到將它們最終以像素輸出到屏幕上這一過程。包括如下幾個部分:html

  1. 構建DOM
    • 將HTML解析成許多Tokens
    • 將Tokens解析成object
    • 將object組合成爲一個DOM樹
  2. 構建CSSOM
    • 解析CSS文件,並構建出一個CSSOM樹(過程相似於DOM構建)
  3. 構建Render Tree
    • 結合DOM和CSSOM構建出一顆Render樹
  4. Layout
    • 計算出元素相對於viewport的相對位置
  5. Paint
    • 將render tree轉換成像素,顯示在屏幕上

值得注意的是,上面的過程並非依次進行的,而是存在必定交叉,後面會詳細解釋。java

想要提升網頁加載速度,提高用戶體驗,就須要在第一次加載時讓重要的元素儘快顯示在屏幕上。而不是等全部元素所有準備就緒再顯示,下面一幅圖說明了這兩種方式的差別。git

構建DOM

DOM (Document Object Model),文檔對象模型,構建DOM是必不可少的一環,瀏覽器從發出請求開始到獲得HTML文件後,第一件事就是解析HTML成許多Tokens,再將Tokens轉換成object,最後將object組合成一顆DOM樹。github

這個過程是一個按部就班的過程,咱們假設HTML文件很大,一個*RTT (Round-Trip Time)*只能獲得一部分,瀏覽器獲得這部分以後就會開始構建DOM,並不會等到整個文檔就位纔開始渲染。這樣作能夠加快構建過程,並且因爲自頂向下構建,所以後面構建的不會對前面的形成影響。web

後面咱們將會提到,CSSOM則必須等到全部字節收到纔開始構建。瀏覽器

構建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,可能會獲得錯誤的結果。

構建Render Tree

這也是關鍵的一步,瀏覽器使用DOM和CSSOM構建出Render Tree。此時不像構建DOM同樣把全部節點構建出來,瀏覽器只構建須要在屏幕上顯示的部分,所以像<head>,<meta>這些標籤就無需構建了。同時,對於display: none的元素,也無需構建。

display: none告訴瀏覽器這個元素無需出如今Render Tree中,可是visibility: hidden只是隱藏了這個元素,可是元素還佔空間,會影響到後面的Layout,所以仍然須要出如今Render Tree中。

構建過程遵循如下步驟

  1. 瀏覽器從DOM樹開始,遍歷每個「可見」節點。
  2. 對於每個"可見"節點,在CSSOM上找到匹配的樣式並應用。
  3. 生成Render Tree。

擴展:CSS匹配規則爲什麼從右向左

相信大多數初學者都會認爲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中)。

Layout

咱們如今爲止已經獲得了全部元素的自身信息,可是還不知道它們相對於Viewport的位置和大小,Layout這一過程須要計算的就是這兩個信息。

根據這兩個信息,Layout輸出元素的Box Model,關於這個,我也寫過一篇文章Understand CSS Formatting Model

目前爲止,如今咱們已經拿到了元素相對於Viewport的詳細信息,全部的值都已經計算爲相對Viewport的精確像素大小和位置,就差顯示了。

Paint

瀏覽器將每個節點以像素顯示在屏幕上,最終咱們看到頁面。

這一過程須要的時間與文檔大小,CSS應用樣式的多少以及複雜度,還有設備自身都有關,例如對簡單的顏色進行Paint是簡單的,可是box-shadow進行paint則是複雜的

引入JavaScript

前面的過程都沒有提到JavaScript,但在現在,JavaScript倒是網頁中不可缺的一部分。這裏對它如何影響CRP作一個概要,具體細節我後面使用Chrome Dev Tools進行了測驗

  1. 解析HTML構建DOM時,遇到JavaScript會被阻塞
  2. JavaScript執行會被CSSOM構建阻塞,也就是說,JavaScript必須等到CSS解析完成後纔會執行(這隻針對在頭部放置<style><link>的狀況,若是放在尾部,瀏覽器剛開始會使用User Agent Style構建CSSOM)
  3. 若是使用異步腳本,腳本的網絡請求優先級下降,且網絡請求期間不阻塞DOM構建,直到請求完成纔開始執行腳本

使用Chrome Dev Tools檢測CRP

爲了模擬真實網絡狀況,我把Demo部署到了個人githubpage,你也能夠在倉庫找到源代碼

同時,不要混淆DOM, CSSOM, Render Tree這三個概念,我剛開始就容易混淆DOM和Render Tree,這兩個是不一樣的

下面的Chrome截圖部分,若是不清晰,請直接點擊圖片查看原圖

0. 代碼部分

HTML

<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>
複製代碼

JavaScript

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);
複製代碼

CSS

/* // [START full] */
body {
  font-size: 16px
}

p {
  font-weight: bold
}

span {
  color: red
}

p span {
  display: none
}

img {
  float: right
}
/* // [END full] */
複製代碼

1. 不加載JS狀況

首先來看沒有加載JS的狀況

上圖中,瀏覽器收到HTML文件後,便開始解析構建DOM。

須要注意,上圖接收的只是圖片的一部分

接下來咱們詳細看看這三個部分:

DOM構建(體現爲parse html)

能夠看出,瀏覽器解析到<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,好比

  1. html文檔中的<style>或者<link>標籤應該放在<head>裏並儘早發現被解析(第4部分我會分析將這兩個標籤放在html文檔後面形成的影響)
  2. 減小第一次請求的CSS文件大小
  3. 甚至能夠將最重要部分的CSS Rule以<style>標籤發在<head>裏,無需網絡請求

頁面首次出現圖片

上圖說明,瀏覽器接收到部分圖片字節後,便開始渲染了,而不是等整張圖片接收完成纔開始渲染,至於渲染次數,本例中的圖片大小爲90KB左右,傳輸了6次,渲染了2次。我以爲這應該和網絡擁塞程度以及圖片大小等因素有關。

還有一點須要注意,兩次渲染中,只有首次渲染引起了Layout和以後的Update Layer Tree,而第二次渲染只有Update Layer Tree,我猜測對於圖片來講,瀏覽器第一次渲染便知道了其大小,因此從新進行Layout並留出足夠空間,以後的渲染只須要在該空間上進行paint便可。整張圖片加載完畢以後,觸發Load事件

上圖包括以後圖片中的Chrome擴展腳本能夠忽視,雖然使用了隱私模式作測驗(避免緩存和一些擴展腳本的影響),但我發現仍是有一個腳本沒法去除,雖然這不影響測驗結果

接下來咱們考慮JavaScript腳本對CRP的影響

2. 引入JS

行內Script (Script位於html尾部)

上圖來看,Parse HTML這一過程被JavaScript執行打斷,並且JavaScript會等待CSSOM構建完成以後再執行,執行完成以後,DOM繼續構建

前面的例子中,咱們看到DOM幾乎都是在CSSOM構建完成前就構建完成了,而引入JS後,DOM構建被JS執行打斷,而JS執行又必須等CSSOM構建完畢,這無疑延長了第一次CRP時間,讓頁面首次出現畫面的時間更長

若是使用外部script腳本,這一時間會更長

外部Script (Script位於html尾部)

對於網絡請求的資源,瀏覽器會爲其分配優先級,優先級越高的資源響應速度更快,時間更短,在這個例子中,CSS的優先級最高,其次是JS,優先級最低的是圖片

咱們主要來看第一部分,後面部分和第1個研究相似

能夠看到,增長了對JS文件的網絡請求時間,一輪CRP時間更長了,對比上面的行內Script可能時間差別沒有那麼明顯,是由於這個例子中的JS文件體積小,傳輸時間只比CSS多一點,主要決定JS什麼時候執行的仍是CSS,若是JS稍大,因爲請求優先級低於CSS,則差別會明顯變大

3. Async Script

若是Script會對頁面首次渲染形成這麼大的影響,有沒有什麼好的辦法解決它呢?

答案是確定的,就是使用異步腳本<script src="" async />

使用異步腳本,其實就是告訴瀏覽器幾件事

  1. 無需阻塞DOM,在對Script執行網絡請求期間能夠繼續構建DOM,直到拿到Script以後再去執行
  2. 將該Script的網絡請求優先級下降,延長響應時間

須要注意以下幾點

  1. 異步腳本是網絡請求期間不阻塞DOM,拿到腳本以後立刻執行,執行時仍是會阻塞DOM,可是因爲響應時間被延長,此時每每DOM已經構建完畢(下面的測驗圖片將會看到,CSSOM也已經構建完畢並且頁面很快就發生第一次渲染),異步腳本的執行發生在第一次渲染以後
  2. 只有外部腳本可使用async關鍵字變成異步,並且注意其與延遲腳本 (<script defer>)的區別,後者是在Document被解析完畢而DOMContentLoaded事件觸發以前執行,前者則是在下載完畢後執行
  3. 對於使用document.createElement建立的<script>,默認就是異步腳本

直接看圖

因爲Script執行修改了DOM和CSSOM,所以從新通過Recalculate Style生成Render Tree,從新計算Layout,從新Paint,最終呈現頁面。因爲這一過程仍然很快(只用了140ms左右),所以咱們仍是察覺不到這個變化

4. CSS在HTML中不一樣位置的影響

前面留下了一個問題,CSSOM沒有構建完成,爲何剛開始的Parse HTML同時就有Recalculate Style這部分?或許這部分會給你一個答案

這裏爲了不JS帶來的影響,使對比更有針對性,刪除了JavaScript

設置style在html文件頭部

先來回顧一下在頭部設置<link>

link tag on top of html file

前面的DOM構建部分出現了Recalculate Style,以後得到CSS並解析後還有一次,一共出現了2次

再來看看改爲<style>,Recalculate Style一共出現1次

<style>在頭部,一開始就直接解析完成,沒有網絡請求

設置style或者link在尾部

先來看看設置<style>在尾部,Recalculate Style出現了1次

再看設置<link>在尾部,Recalculate Style一共出現3次

先總結實驗結果

實驗中將<link>放在頭部,<style>放在頭部,<link>放在尾部,<style>放在尾部,Recalculate Style的次數分別是2,1,3,1

而後咱們須要瞭解Chrome Dev Tools Performance Tab的幾個關鍵過程

  1. Performance Tab裏的Recalculate Style,官方是這樣解釋的

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的構建

  1. 對於<style>裏的CSS,解析過程發生在Recalculate Style中,而<link>得到的CSS,解析過程是單獨的,叫作Parse CSS (和Parse HTML相似)

  2. 同時,要明確瀏覽器還有一個默認的User Agent Style,咱們的Style只是對其進行一個覆蓋

最後猜測這4個結果的緣由以下

  1. 瀏覽器若是發現<head>裏存在<link>,則會等待CSS網絡請求完成並解析好以後纔開始Render Tree,至於第一次的Recalculate Style,我猜測是默認的User Agent Style,此時CSSOM已經開始構建了,而接收到CSS文件,咱們設置的Style會對默認的Style進行覆蓋。這裏第一次Recalculate Style只包含CSSOM構建,第二次則包含了CSSOM更新以及Render Tree構建
  2. <style>放在頭部,瀏覽器由於能夠立刻拿到CSS,就能夠立刻進行解析,此時User Agent Style的解析和咱們自定義的Style解析合併,Recalculate Style包含了CSSOM構建和Render Tree構建
  3. <style>放在尾部,和放在頭部相似。只不過晚點發現CSS,可是因爲是行內<style>,仍是能夠立刻解析
  4. <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的實踐。

疏漏之處,歡迎指正。

參考

  1. developers.google.com/web/fundame… (Recommended)
  2. Website Performance Optimization - Udacity
  3. stackoverflow.com/questions/5…
  4. www.youtube.com/watch?v=PkO…
相關文章
相關標籤/搜索