爲了把HTML、CSS和JavaScript轉化成活靈活現、絢麗多彩的網頁,瀏覽器須要處理一系列的中間過程,優化性能其實就是了解這個過程當中發生了什麼-即CRP(Critical Rendering Path,關鍵渲染路徑)。首先,咱們從頭開始快速學習一下瀏覽器是如何顯示一個簡單網頁的。css
<html> <head> <meta name="viewport" content="width=device-width,initial-scale=1"> <link href="style.css" rel="stylesheet"> <title>Critical Path</title> </head> <body> <p>Hello <span>web performance</span> students!</p> <div><img src="awesome-photo.jpg"></div> </body> </html>
一個普通的頁面,裏面包含一些文本和一張圖片,瀏覽器是如何處理這個頁面的呢?html
轉換:瀏覽器從磁盤或網絡讀取HTML的原始字節,而後根據指定的文件編碼格式(例如 UTF-8)將其轉換爲相應字符html5
令牌化:瀏覽器把字符轉化成W3C HTML5 標準指定的各類確切的令牌,好比"<html>"、"<body>"以及其餘在尖括號內的字符串。每一個令牌都有特殊的含義以及它本身的一套規則web
詞法分析:生成的令牌轉化爲對象,這個對象定義了它們的屬性及規則segmentfault
DOM構建:最後,因爲HTML標記定義了不一樣標籤之間的關係(某些標籤嵌套在其餘標籤中),建立的對象在樹狀的數據結構中互相連接,樹狀數據結構也捕獲了原始標籤訂義的父子關係:HTML對象是body對象的父對象,body是p對象的父對象等等瀏覽器
上述整個流程的最終輸出是文檔對象模型,即這個簡單網頁的 "DOM",瀏覽器完成頁面的全部後續處理都是創建在這個DOM基礎上的。
打開Chrome DevTools > Timeline,錄製時間軸,上述過程對應Loading事件中的Parse HTML事件,能夠查看到執行這一過程所須要的時間。
DOM樹捕獲了文檔標記的屬性及關係,但它沒有告訴咱們元素在渲染時是什麼樣子的。這是CSSOM要乾的活,也就是接下來要講的。服務器
當瀏覽器構建上述網頁DOM的時候,在head裏面碰到一個link標籤,這個標籤引用了一個外部的CSS樣式表:style.css。瀏覽器預測會須要這個資源來渲染頁面,所以會當即發出一個該資源的請求,該請求返回如下內容:網絡
body { font-size: 16px } p { font-weight: bold } span { color: red } p span { display: none } img { float: right }
與HTML同樣,咱們須要將收到的 CSS 規則轉換爲瀏覽器能夠理解、可以處理的東西。所以,咱們重複與處理 HTML 很是類似的過程:數據結構
最終輸出的是CSS對象模型,即CSSOM。app
想要查看CSS處理過程所花費的時間,能夠在錄製的時間軸中查看Rendering事件中的Recalculate Style事件:與DOM解析不一樣,timeline不顯示單獨的「Parse CSS」條目,而是在Recalculate Style事件下一同捕獲CSS解析、CSSOM構建以及computed styles的遞歸計算。
前面介紹了咱們根據輸入的HTML及CSS構建了DOM樹和CSSOM樹,但兩者是獨立的對象:DOM描述的是文檔內容,CSSOM描述的是應用於文檔的樣式規則。瀏覽器會把DOM和CSSOM組合起來構建一個渲染樹(Render-tree),渲染樹會捕獲頁面上全部可見的DOM內容以及應用在每一個節點上的CSSOM樣式。
構建渲染樹的過程大體以下:
從DOM樹的根節點開始,遍歷每一個可見的節點
某些節點不可見(例如 script 標籤、meta 標籤等),由於它們不會體如今渲染結果中,因此會被忽略
某些經過 CSS 隱藏的節點在渲染樹中也會被忽略,好比應用了 display:none 規則的節點
爲每個可見的節點匹配並應用對應的CSSOM規則
生成有內容和計算樣式的可見節點
小提示:注意
visibility: hidden
和display: none
兩者的區別。visibility: hidden
只是讓元素在視覺上不可見,可是元素在頁面佈局中仍然佔據空間。而display: none
則是從渲染樹中刪除某一個元素,不只視覺上不可見,渲染樹上也沒有,更不會影響到頁面的佈局。
最終輸出的就是一個包含了全部可見節點的內容及樣式信息的渲染樹。
到目前爲止,咱們已經計算出了哪些節點是可見的以及它們的計算樣式,可是咱們尚未計算它們在設備視口(viewport)中的準確位置及尺寸大小——這就是佈局(Layout)階段要作的工做,也就是常說的重排(reflow)。
爲了計算出頁面中每一個對象的準確大小和位置,瀏覽器從渲染樹的根節點開始遍歷,計算頁面上每一個對象的幾何信息。舉例以下:
<html> <head> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>Critial Path: Hello world!</title> </head> <body> <div style="width: 50%"> <div style="width: 50%">Hello world!</div> </div> </body> </html>
上面頁面的 body 包含兩個嵌套 div:第一個 div(父元素)將節點尺寸大小設置爲視口寬度的 50%,第二個 div 的寬度爲父元素的 50%,即視口寬度的 25%!
佈局過程的輸出是一個「盒子模型」,它精確地捕獲了每一個元素在視口中的準確位置及尺寸大小:全部相對度量單位都被轉換爲屏幕上的絕對像素。
自此,咱們已經知道了哪些節點是可見的以及它們的計算樣式和幾何信息,而後咱們就能夠把這些信息傳送到最後一個階段,即把渲染樹中的每個節點都轉化到屏幕上實際的像素點。這個步驟一般被稱爲繪製(painting)或者柵格化(rasterizing)。
構建渲染樹、佈局與繪製所消耗的時間也能夠經過timeline來查看:
"Layout" 事件捕獲渲染樹的構建及位置、尺寸的計算
佈局完成時,瀏覽器會觸發 'Paint' 事件:將渲染樹轉化爲屏幕上的實際像素
終於,咱們的頁面在設備視口中可見了。
如今回顧一下瀏覽器執行的幾個步驟:
處理 HTML 標記,構建 DOM 樹
處理 CSS 標記,構建 CSSOM 樹
將 DOM 樹和 CSSOM 樹融合成渲染樹
根據渲染樹進行佈局,計算每一個節點的幾何信息
在屏幕上繪製各個節點
優化關鍵渲染路徑即儘量地縮短上述第 1 步到第 5 步耗費的總時間。
在構建渲染樹部分咱們已瞭解到:CRP要求DOM和CSSOM二者融合在一塊兒才能構建渲染樹。這就致使了一個性能問題:HTML和CSS都是阻塞渲染的資源。HTML很顯然,沒有DOM就沒有內容去渲染。CSS沒有那麼明顯,但確實是阻塞渲染的資源。咱們知道一個正常的網頁若是沒有引入專用的css,頁面有多醜陋。當咱們的網頁引入了專用的css,頁面一加載出來的時候就是絢麗多彩的,若是css不阻塞渲染,咱們看到的極可能是這樣的一個畫面:頁面剛加載出來的時候其醜無比,過了一會,頁面又變漂亮了……
既然CSS是阻塞渲染的資源,這就意味着在CSSOM構建完成以前,瀏覽器不會去渲染任何已處理的內容。要儘早、儘快地把CSS下載到客戶端以優化首次渲染的時間。
使用CSS「媒體類型」和「媒體查詢」優化阻塞渲染的CSS:
<link href="style.css" rel="stylesheet"> <link href="print.css" rel="stylesheet" media="print"> <link href="other.css" rel="stylesheet" media="(min-width: 40em)">
第一條聲明阻塞渲染,匹配全部狀況
第二條聲明只適用於打印(媒體類型),所以,頁面在瀏覽器中首次加載時,不會阻塞渲染
第三條聲明提供了媒體查詢,由瀏覽器判斷:若是條件符合,則在該樣式表下載並處理完之前,瀏覽器會阻塞渲染
小提示:「阻塞渲染」僅是指該資源是否會阻塞瀏覽器的首次頁面渲染。不管 CSS 是否阻塞渲染,CSS 資源都會被下載,只是說非阻塞性資源的優先級比較低而已。
JS能夠修改頁面的內容、樣式以及響應用戶的交互,JS在DOM、CSSOM和JS執行之間引入了不少新的依賴關係,致使瀏覽器在處理和渲染頁面上出現大幅延遲:
當瀏覽器遇到<script>標籤時,DOM構建會暫停,直到腳本執行完畢
JavaScript 執行會暫停,直到CSSOM準備就緒
默認狀況下,JavaScript 執行會阻塞解析器:當瀏覽器在文檔中遇到<script>標籤時,DOM構建必須暫停,瀏覽器把控制權移交給JS引擎,JS引擎編譯並執行腳本,腳本執行完畢後再繼續構建DOM。
事實上,內聯腳本始終會阻塞解析器,除非你編寫額外的代碼來延遲它們的執行。那經過<script>引入的外聯腳本呢?結果是同樣的,瀏覽器都會暫停,而後執行腳本,腳本執行完畢以後再去處理文檔的剩餘部分。儘管如此,經過<script>引入外聯腳本仍是有一個很大的好處。
默認狀況下,全部 JS 均會阻塞解析器,由於瀏覽器不知道腳本想在頁面上作什麼,所以它必須假定最糟的狀況並阻塞解析器。可是,若是咱們可以有個信號告知瀏覽器,說腳本無需在文檔中引用它的確切位置被執行呢?這樣一來,瀏覽器就會繼續構建DOM,並在腳本準備就緒後執行腳本。
這個信號就是async——在script標籤裏面添加async關鍵字,其有兩個特性:
告訴瀏覽器當它碰到<script>標籤時不用阻塞DOM構建,所以瀏覽器會忽略腳本請求,繼續解析DOM
JS執行不依賴CSSOM:若是在CSSOM就緒以前腳本已經就緒,腳本能夠當即執行
很顯然,這將會顯著提高性能!
小提示:另外一個信號是defer,關於二者的區別能夠參考一下這個問題的答案defer和async的區別
先定義三個用於描述CRP的詞彙:
關鍵資源:可以阻止網頁首次渲染的資源
關鍵路徑長度:往返過程的數量,或者獲取全部關鍵資源所需的總時間
關鍵字節:網頁首次渲染所需的總字節數,是全部關鍵資源的傳輸文件大小總和。
demo1:
<html> <head> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>Critical Path: No Style</title> </head> <body> <p>Hello <span>web performance</span> students!</p> <div><img src="awesome-photo.jpg"></div> </body> </html>
最簡單的可用網頁僅由 HTML 標記組成:無 CSS、JavaScript 或其餘類型的資源。要呈現此網頁,瀏覽器必須初始化請求、等待 HTML 文檔準備就緒、對其進行解析、構建 DOM,最後使其呈如今屏幕上。
1個關鍵資源
1個關鍵路徑長度(假設文件很小)
5KB關鍵字節
T0 和 T1 之間的時間用於捕獲網絡傳輸和服務器處理時間。 在最理想的狀況(HTML 文件較小)下,咱們僅需一個網絡往返過程便可提取整個文檔(因爲 TCP 傳輸協議的工做方式,較大的文件可能須要多個往返過程)。
demo2:
<html> <head> <meta name="viewport" content="width=device-width,initial-scale=1"> <link href="style.css" rel="stylesheet"> </head> <body> <p>Hello <span>web performance</span> students!</p> <div><img src="awesome-photo.jpg"></div> </body> </html>
2個關鍵資源
2個或更多個關鍵路徑長度
9KB關鍵字節
咱們必須同時使用 HTML 和 CSS 來構建渲染樹,所以 HTML 和 CSS 均爲關鍵資源;瀏覽器須要一個網絡往返過程來提取 HTML 文檔,而後檢索到的標記告知咱們還須要 CSS 文件,這意味着,瀏覽器必須返回服務器並獲取 CSS,而後才能在屏幕上呈現網頁,所以,該網頁最少須要兩個往返過程才能顯示(CSS 文件可能須要多個往返過程,重點在'最少');兩種資源加起來的關鍵字節總量最多爲 9 KB。
demo3:
<html> <head> <meta name="viewport" content="width=device-width,initial-scale=1"> <link href="style.css" rel="stylesheet"> </head> <body> <p>Hello <span>web performance</span> students!</p> <div><img src="awesome-photo.jpg"></div> <script src="app.js"></script> </body> </html>
3個關鍵資源
2個或更多個關鍵路徑長度
11KB關鍵字節
咱們有三個關鍵資源,關鍵字節總量最多爲 11 KB,可是關鍵路徑長度仍然是兩個往返過程,由於咱們能夠並行傳輸 CSS 和 JavaScript!
demo4:
若是app.js中的內容不涉及到操做DOM和CSSOM,只是一些分析類型的代碼和其餘不須要阻塞頁面渲染的代碼,則能夠在<script>中加入「async」屬性:
<html> <head> <meta name="viewport" content="width=device-width,initial-scale=1"> <link href="style.css" rel="stylesheet"> </head> <body> <p>Hello <span>web performance</span> students!</p> <div><img src="awesome-photo.jpg"></div> <script src="app.js" async></script> </body> </html>
異步執行腳本有如下幾項優點:
腳本不再會阻止解析器,因此也再也不是CRP的組成部分
由於沒有其餘關鍵腳本,CSS 也不須要阻止DomContentLoaded
事件
DomContentLoaded
事件觸發得越早,其餘應用邏輯執行的時間就越早
所以,通過優化的網頁恢復到了具備兩個關鍵資源(HTML 和 CSS)、具備兩個往返過程的最短關鍵路徑長度和 9 KB 的關鍵字節總量。
demo5:
若是CSS樣式表僅適用於打印:
<html> <head> <meta name="viewport" content="width=device-width,initial-scale=1"> <link href="style.css" rel="stylesheet" media="print"> </head> <body> <p>Hello <span>web performance</span> students!</p> <div><img src="awesome-photo.jpg"></div> <script src="app.js" async></script> </body> </html>
由於 style.css 資源僅用於打印,因此只要DOM構建完成,瀏覽器就具備了渲染網頁的足夠信息! 因此,該網頁僅具備一個關鍵資源(HTML),最小關鍵呈現路徑長度爲一個往返過程和5KB的關鍵字節。
常規步驟:
分析、描述關鍵路徑:關鍵資源數量、字節數、關鍵路徑長度
最小化關鍵資源數量:刪除相應資源、延遲下載、標記爲異步資源等
減小關鍵字節數,以減小資源下載時間(往返次數)
優化剩餘關鍵資源的加載順序:儘量早的下載全部關鍵資源,以縮短關鍵路徑長度
References