我在看了 google 的 Critical Rendering Path (中文)後, 想把 CRP(Critical Rendering Path) 用通俗易懂的方式描述出來。 官方文檔固然是描述最爲詳盡且可靠的。 文章裏的有些圖片是直接引用自官方文檔。 若是存在侵權, 馬上刪除。css
遊覽器從開始請求 HTML 文檔, 到首次渲染到屏幕上(首屏), 背後須要作不少的事情, 這一連串事情就是 CRP 。 開發 app 的時候不少優化都是和縮短 CRP 有關的。 縮短 CRP 的長度可以有效下降首屏時間。 這也是理解 CRP 最大的好處。html
CRP 大概包括兩個部分: 一、 本地加載渲染, 二、 網絡請求 。前端
從遊覽器加載 HTML 文檔到首屏, 中間發生了什麼? web
TL;DRchrome
遊覽器解析 HTML 造成 DOM 樹。瀏覽器
遊覽器解析 CSS 代碼輸出 CSSOM 。網絡
DOM 和 CSSOM 一塊兒構造出一顆 Render Tree 。app
遊覽器對 Render Tree 上的節點計算精確位置(佈局)。框架
使用 Render Tree 繪製 (Painting) 到屏幕上。dom
DOM 用來描述文檔結構, 於是是樹形結構。 getElementById , querySelector 等 API 就是針對DOM操做的。 CSSOM 用來描繪 DOM 上的節點的樣式, 因此圖中的 CSSOM 的根節點是 body , 這是由於 head 標籤沒有樣式信息。 樣式的繼承過程就是發生在構造 CSSOM 的階段。 當你使用 element.style 訪問元素的樣式的時候, 其實訪問的是CSSOM 上對應的節點。 而後利用 DOM 和 CSSOM 生成 Render Tree , 這中間作了不少事情, 好比說若是檢測到一個元素所對應的 CSSOM 節點存在 display: none
, 那這個節點將不會輸出到 Render Tree 。
接下來要利用 Render Tree 計算每一個節點在視口上具體的位置, 這就是佈局。 佈局階段輸出 CSS 的 「盒子模型」, 最後利用這些盒子, 繪製到屏幕上 (Painting) , 在這個過程當中若是檢測到 visibility: hidden
, 遊覽器會將其繪製成一個空白(這裏的空白是完徹底全的空白, 而不是白色。。。)
如今對於 visibility: hidden
和 display: none
的理解應該加深了很多, 具體的區別能夠 google 。
這就是遊覽器的大概的渲染過程。
其實討論 CRP 更多的時候是在討 CSS 和 JS 。 由於 CSS 和 JS 會阻塞渲染。 爲何說 CSS 和 JS 會阻塞渲染? 很難想象若是遊覽器先呈現一段沒有樣式的頁面,而後忽然刷新, 這是一種及其糟糕的體驗。 而 JS 腳本的執行會訪問 DOM 和 CSSOM, 爲何 JS 是同步加載, 而不是異步加載呢? 遊覽器爲何不像處理樣式文件同樣處理腳本文件呢? 這其實很好理解, 腳本文件通常包含着你應用的邏輯, 若是腳本都是異步加載, 那應用的邏輯豈不是亂套。。
什麼是 CRP 長度?
獲取全部阻塞資源(關鍵資源)所需的往返次數, 好比說樣式文件, 腳本文件; 圖片不屬於關鍵資源, 由於圖片不會致使阻塞遊覽器渲染。
關於 CRP 中幾個關鍵的時間點:
domLoading 這是整個 CRP 的開始的時間點。
domInteractive 遊覽器恰好構建完 DOM 的時間點。
domContentLoaded DOM 構建完成, 且沒有任何樣式會阻塞腳本容許的時間點。 意思就是當沒有腳本文件執行的時候, DOM 構建完成時就到達這個時間點, 畢竟沒有腳本須要執行怎麼會存在阻塞呢? 不過這種狀況不多見。 當有腳本文件要執行的時候, 樣式( CSSOM )會阻塞腳本文件執行, 此時要等到樣式所有就緒的時候纔會到達這個時間點。 因此, 當存在腳本文件的時候, 通常 domContentLoaded 會日後推移許多。
domCompelete 表示全部資源都已經下載完成, 包括圖片, 字體等。 這個時間點將觸發 onload 事件。
如圖:
talk is cheap show me the code
HTML 文件:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" href="./style.css"> <title>Test</title> </head> <body> <div> <figure> <img id="emma" src="http://photocdn.sohu.com/20150227/Img409195726.jpg" alt="emma"> <figcaption>Emma.Watson</figcaption> </figure> <button id="switch">Switch</button> </div> <script src="./index.js"></script> </body> </html>
這段代碼首先在 head 裏引用了樣式文件, 在文檔的最後引用了 script 文件, 這是由於 script 標籤會致使遊覽器阻塞 DOM 的解析, 遊覽器甚至會花時間等待 script 引用的腳本資源的請求過程(同步加載), 直至請求響應後且解析完腳本後纔會將控制權交還給遊覽器來繼續解析 DOM 。 所以腳本執行時, 極可能 DOM 沒有建立完成, 將 script 放到最後是明智之舉。 這裏一共請求了三個資源, 可是幾乎都是同時發起的, 所以算做一次往返, 因此 CRP 長度是1。
使用 DevTools 的 Timeline 查看頁面加載狀況。 若是你對 DevTools 不熟悉, 能夠看下 Chrome DevTools 快速入門 的官網文檔 。 實際上這裏並用不到不少這方面的知識。 稍微有所瞭解就行。 在使用 Timeline 的時候要注意開啓隱身模式, 否則會存在諸如遊覽器插件等干擾因素。
上面的代碼的加載性能截圖:
能夠看到圖中有兩條很是接近的垂直的線, 藍色表示觸發 DOMContentLoaded 事件的時間點, 紅色表示觸發 onload 事件的時間點。兩個時間點很是接近, 這是因爲遊覽器在請求外部腳本時會等待其響應, 所以將 DOMContentLoaded 事件向後推遲了。 這裏的 CRP 長度是1。 假如沒有腳本文件的請求, 那咱們應用的 DOMContentLoaded 將在 DOM 解析完成後發生, 而不至於等到全部樣式就緒( CSS 解析完成)。 這裏即使是將腳本文件內聯到 HTML 文檔中也是同樣的結果。 由於腳本代碼的執行會訪問 CSSOM , 遊覽器在解析腳本代碼的時候會等待全部 CSSOM 就緒纔開始執行腳本代碼。 所以效果是同樣的。
這是上面代碼的 CRP 流程圖:
能夠看到在 T1 到 T2 這個時間段, 遊覽器解析 DOM 的操做是被阻塞的。 即使內聯了腳本文件。
由於大多數的 JavaScript 框架的邏輯開始部分都是從 DOMContentLoaded 事件點開始的(由於 DOMContentLoaded 總比 onload 快, onload 會等到圖片等資源都就緒才觸發, 會拖慢首屏時間)。 所以, 提早 DOMContentLoaded 是頗有必要的。
通常的 CRP 流程是這樣的:
圖上有兩個灰色的方塊, 這兩個方塊是致使 CRP 時間變長的罪魁禍首!
縮短 CRP 就是儘快使 DOMContentLoaded 事件產生, 以便儘快執行 APP 邏輯, 不要拖到 onload , 節約一分一秒! 由於 DOMContentLoaded 產生後, 就會構建 Render Tree , 而後順水推舟, 用戶就能看到 APP 了。
不要引用過多的腳本文件!
在前幾年的前端開發中, 喜歡將對腳本的引用寫在 HTML 文件裏, 這會致使 DOMContentLoaded 觸發大大延遲。 由於每一次解析一個腳本都會阻塞遊覽器構建 DOM 。 當一個 HTML 文件引用幾十個外部腳本文件時, 那確定是一場災難! 所幸最近幾年各類前端打包工具使得這個問題得以解決。
在筆試騰訊的時候遇到一個問題: 使用 HTTP2 的時候還有沒有將 JS 文件打包的必要? 答案是: 有必要, 並且是必須的! 雖然 HTTP2 能夠實現同一個 TCP 鏈接裏的全部資源的並行傳輸(使用流)。 可是遊覽器處理外部腳本文件的策略不會變! 因此依然有打包的必要!
將外部腳本的引用放到 HTML 文件的最後, 或者使用 defer 屬性。
即使你使用打包工具只引用了一個外部腳本文件, 可是若是這個腳本文件的傳輸延遲和執行延遲, 會致使後面的非關鍵資源的請求被延遲, 雖然這不會減慢 APP 的首屏時間。 可是圖片等非關鍵資源的呈現時間卻被延遲了。 使用defer屬性能夠將執行時間推遲到 domContentLoaded 時間點後。 DOMContentLoaded也是這個時間點後觸發。那會先執行 defer 仍是先觸發 DOMContentLoaded 事件? 在 Chrome 遊覽器下的順序是先執行 defer 再觸發DOMContentLoaded 事件。
對外部腳本使用 async 屬性
async 屬性告訴遊覽器異步請求外部腳本文件, 不要阻塞在這裏。 這樣可使得遊覽器繼續構建 DOM 。 或者處理後面的資源請求。
將樣式文件請求置於 head 標籤內。
儘早在 HTML 文檔內指定全部 CSS 資源,以便瀏覽器儘早發現 link 標記並儘早發出 CSS 請求。
儘可能不要使用 @import
指令。
CSS 中的 @import
表示在一個樣式文件中導入另一個樣式文件。 一個樣式文件 import 另一個樣式文件時, 只有在這個樣式文件被收到且被解析完成纔會 import 。 這樣會增長 CRP 長度。
內聯樣式
使用 style 標籤將樣式內聯到 HTML 文件內, 雖然這樣作會增大 HTML 文件的體積。 可是卻能夠減小 CRP 長度。 同時也避免了腳本代碼的執行被阻塞的狀況。
爲何沒有內聯腳本呢? 由於絕大多數狀況下不可能作到徹底的內聯樣式。 這個時候即使使用內聯腳本也會由於 CSSOM 而被阻塞, 會阻止 DOM 構建。 於是這並非一種優化措施。
減小你的關鍵資源的大小和數量
這是優化最好的手段。
Else
這裏並無提到如何去優化應用的邏輯來增長速度, 由於這並不屬於 CRP 。
Lighthouse 是一個網絡應用審覈工具, 能夠幫助你找到 CRP 的瓶頸所在。
Lighthouse 的截圖:
這個內置的 API 能夠幫助你記錄下各個時間點的具體時間值, 上面說的那幾個時間點都有。
若是你以爲有哪裏寫的不是很恰當或你不能理解的, 能夠在評論裏告訴我。。
若是你以爲我寫的內容對你有所幫助。 能夠選擇關注我。
個人博客地址是: mrcode