前言
First paint 直譯過來的意思就是瀏覽器第一次渲染(paint),在First paint之前是白屏,在這個時間點之后用戶就能看到(部分)頁面內容。
所以研究這個First Paint的觸發時機對于優化瀏覽器頁面的首屏渲染時間有很重要的作用。
在正題開始之前,先說下瀏覽器的頁面的加載流程(大體過程是這樣,并不精確,只是為了幫助理解后面內容):
- 瀏覽器輸入url,瀏覽器發送請求到服務器,服務器將請求的html返回給瀏覽器。
- 瀏覽器下載完成HTML(Finish Loading HTML)之后,便開始從上到下解析。
- 解析的過程中碰到css和js外鏈(其實HTML的下載也是這個流程)都會執行以下過程:
- Send Request:表示給這個外鏈對應的服務器發送請求
- Receive Response: 表示接收響應,這里是表示告訴瀏覽器可以開始從網絡接收數據了
- Receive Data:表示開始接收數據
- Finish Loading: 表示已經完成下載數據。
- Parse Stylesheet/Evaluate(默認情況下js下載完成之后執行Evaluate,css下載完成后會進行Parse Stylesheet)
- 所有的css下載完成后Parse Stylesheet然后開始構建CSSOM
- DOM(文檔對象模型)和 CSSOM(CSS對象模型)會合并生成一個渲染樹(Render Tree)
- 根據渲染樹的內容計算處各個節點在網頁中的大小和位置(Layout,可以理解為“刻章”)
- 根據Layout繪制內容在瀏覽器上(Paint,可以理解為“蓋章”)。
正題開始
在最新版的Chrome的perfomance中是能直接看到First Paint這個時間點的,為了方便大家測試,我就直接拿谷歌這個示例頁面來做演示:
測試頁面
用chrome打開上面鏈接,最好是隱身模式,防止插件亂入影響判斷,按F12或者右鍵檢查元素打開控制臺先切換到Network選項,勾選禁用緩存(緩存也會影響到判斷):
切換到Perfomance,勾選Screenshots并點擊紅框進行頁面分析(會自動停止的,不用點stop):
分析完后可以看到如下結果:
上圖中的綠色的線就是當前頁面第一次出現內容的時間點,可以將鼠標放到Main上面的Network中綠色的線附近可以看到在他之前頁面空白,在他之后就有內容。 除了綠色的線還有藍色以及紅色的線,這里也解釋一下:
簡單講一下DOMContentLoaded、load的區別:
- DOMContentLoaded是HTML文檔(包括CSS、JS)被加載以及解析完成之后觸發(即 HTML->DOM的過程完成 )
- load則是在頁面的其他資源如圖片、字體、音頻、視頻加載完成之后觸發
- load事件一般在DOMContentLoaded之后才觸發(也有可能在它之前哦)
這個時候發現綠色虛線之前有一個淺綠色方塊,相應的解釋如下:
由圖可以得出“淺綠色”代表的是根據CSSOM計算樣式并進行布局繪制的過程,這段時間內瀏覽器做了一下事情:
- Recalculate Style:重新計算樣式,確定DOM元素的樣式規則(定規則)
- Layout:根據計算結果進行布局,確定元素的大小和位置(刻章)
- Update Layer Tree: 更新渲染層樹
- Paint: 繪制,根據前面的Layer Tree繪制頁面(位置、大小、顏色、邊框、陰影等)(蓋章)
- Composite Layers: 形成層,瀏覽器按照合理的順序合并成一個圖層然后輸出到屏幕(給別人看)
那什么時候開始First paint呢?在淺綠色方塊最前面的虛線往前看,發現在灰色虛線之前都會有一個步驟:就是Parse Stylesheet(調研了很多頁面都是如此)
所以,First Paint的加載流程應該是這樣:
- 所有的CSS加載完成
- Parse Stylesheet:構建出CSSOM
- Recalculate Style:重新計算樣式,確定DOM元素的樣式規則(定規則)
- Layout:根據計算結果進行布局,確定元素的大小和位置(刻章)
- Update Layer Tree:更新渲染層樹
- Paint:繪制,根據前面的Layer Tree繪制頁面(位置、大小、顏色、邊框、陰影等)(蓋章)
- Composite Layers:形成層,瀏覽器按照合理的順序合并成一個圖層然后輸出到屏幕(給別人看)
但是現在還只是確定了First Paint的加載流程,也確定了他是在所有CSS執行完Parse Stylesheet之后才會觸發,但是這還是不夠準確啊,所以我找了一些CSS和JS的外鏈來測試,模板如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> <link href="https://cdn.bootcss.com/bootstrap/4.0.0/css/bootstrap.css" rel="stylesheet"> <link href="https://cdn.bootcss.com/jqueryui/1.12.1/jquery-ui.css" rel="stylesheet"> <script src="https://cdn.bootcss.com/vue/2.5.13/vue.js"></script> <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script> <script src="https://cdn.bootcss.com/react/16.4.0-alpha.0911da3/cjs/react.development.js"></script> <script src="https://cdn.bootcss.com/angular.js/2.0.0-beta.17/angular2.js"></script> </head> <body> <div id='root1'> 1 </div> <div id='root2'> 2 </div> <div id='root3'> 3 </div> </body> </html>
我們通過改變上面模板里的外鏈順序來探究:
第一種情況:
發現FP發生在最后(實心的藍色線是按shift出來的,不是DOMContentLoaded),現在還發現不了什么。
第二種情況:
調換head中CSS和JS外鏈位置
仍然發現不了什么
第三種情況
把CSS放head,JS放</body>前
發現FP竟然在藍色和紅色虛線前面出現,通過這點可以確定,FP還跟JS外鏈的位置有關,繼續:
第四種情況:
JS外鏈放head,CSS放</body>前
發現又跟第一二種情況一樣了,所以這種用法是不可取的。
第五種情況:
CSS和JS都放</body>前,且CSS緊貼在div后面,JS在CSS后面:
可以發現FP居然更快觸發,但是我鼠標hover到綠色虛線后,仍然是白屏,只有等到CSS加載完成執行Parse Stylesheet之后才顯示出內容(說明這種用法也不可取),難道body中的CSS也會影響?
第六種情況:
掉換一下上面CSS和JS的位置:
發現這次FP觸發而且立馬有內容,而等到CSS加載完成之后還會再重新渲染一次,嗯,看來body中的第一個JS腳本有貓膩,接下來的情況對他特殊照顧。
第七種情況:
CSS放head中,JS放在div節點中間:
哈哈,居然只渲染了12倆字,說明瀏覽器會渲染body中腳本之前的內容,那會是哪個腳本之前的內容呢?
第八種情況:
在div之間都插入腳本
看來瀏覽器會提前渲染body中第一個腳本前的內容(我們就把body中的第一個外鏈腳本叫做【第一腳本】吧),并且第一腳本還會在FP之后才執行。所以結合之前得出的結論,在CSSOM準備就緒之后,瀏覽器會提前渲染第一腳本前的內容,我們可以用第九種情況來驗證:
第九種情況:
這種情況和上種沒什么區別,只是增加了一個CSS,這個CSS中還會發出一個請求去加載其他CSS(通過@import url()的方式),所以CSS的加載時間很長。
通過結果可以看出,123在CSS下載完成之后才渲染,而不是單獨渲染一個1,所以FP必須得等到CSSOM準備就緒之后才會觸發,否則即使有第一腳本在也沒用。 所以到這里,我們總算可以下結論了:
FP發生在body中第一個script腳本之前的CSS解析和JS執行完成之后。換句話說就是第一腳本之前的DOM和CSSOM準備就緒之后,便會著手渲染第一腳本前的內容。
但是...你以為到這里就結束了?其實沒有。
第十種情況:
這種情況中,head中既有JS也有CSS,body中也有第一腳本存在:
注意上圖中的vue.js是在head中的,而后面的JS文件都在body中,而且,vue.js加載完成之后,body中的JS還沒下載完成,這個時候我們調換一下vue.js和angular2.js的位置:
看,這個時候又沒有提前渲染了,123等到所有JS文件都執行完之后才渲染,這種情況除了驗證了第九點的結論,還能補充我們的結論:
如果第一腳本前的JS和CSS加載完了,body中的腳本還未下載完成,那么瀏覽器就會利用構建好的局部CSSOM和DOM提前渲染第一腳本前的內容(觸發FP);如果第一腳本前的JS和CSS都還沒下載完成,body中的腳本就已經下載完了,那么瀏覽器就會在所有JS腳本都執行完之后才觸發FP。
到這里本次探究就結束了,其實還有很多種情況,感興趣的可以自己去試試。
建議:
- CSS放在head中,JS放在</body>前(如果在head必須放JS,也盡量減少他的大小,把大JS文件放</body>前)。
- 減小head中CSS和JS大小(gzip了解一下?),
- 優化head中的JS和CSS外鏈的網絡情況,減少Stalled、TTFB和Content Download的時間。
- 在第一腳本前使用骨架圖,可以減少用戶的白屏感知時間(對于使用JS插入模板來渲染的框架,建議將骨架圖的路由生成邏輯單獨提出來)
科普一下
- Chrome會渲染局部CSSOM和DOM
- First Paint和DOMContentLoaded、load事件的觸發沒有絕對的關系,FP可能在他們之前,也可能在他們之后,這取決于影響他們觸發的因素的各自時間(FP:第一腳本前CSSOM和DOM的構建速度;DOMContentLoaded:HTML文檔自身以及HTML文檔中所有JS、CSS的加載速度;load:圖片、音頻、視頻、字體的加載速度)。
- DOMContentLoaded和load事件也沒有強制的先后順序,DOMContentLoaded一般在load事件之前觸發,但也可能在load事件之后觸發。
- 第一腳本前的CSS如果還會去加載字體文件,那么即使CSSOM和DOM構建完成觸發FP,頁面內容也會是空白,只有等到字體文件下載完成才會出現內容(這也是我們在打開一個加載了谷歌字體的網站會白屏很長時間的原因)。
- 默認情況下,CSS外鏈之間是誰先加載完成誰先解析,但是JS外鏈之間即使先加載完成,也得按順序執行。
- link外鏈后面緊跟script外鏈,須先等link parse完成之后,script才會執行,即使script先下載完成。script后面緊跟link,也是一樣,會等script執行完之后,link才會parse。
- 如果script之后緊跟幾個link且script比這幾個link的下載時間都長,那script執行完成之后link是按順序執行。
- RRDL:
- R:send Request,發送資源請求
- R:receive Response,接收到服務端響應
- D:receive Data,開始接受服務端數據(一個資源可能執行多次)
- L:finish Loading,完成資源下載
- 瀏覽器在RRDL的時候,在D(Receive data)這個步驟可能執行多次。
- TTFB:Time To First Byte,第一個字節返回的時間,這個是對應send Request到receive Response這段時間。
- 瀏覽器會給HTML中的資源文件進行等級分類(Hightest/High/Meduim/Low/Lowest),一般HTML文檔自身、head中的CSS都是Hightest,head中JS一般是High,而圖片一般是Low,而設置了async/defer的腳本一般是Low,gif圖片一般是Lowest。
- 下圖中的資源文件淺色和深色和第二個圖畫紅框的位置是對應的(不信自己計算一下對應的時間)
希望本文能幫助到您!
點贊+轉發,讓更多的人也能看到這篇內容(收藏不點贊,都是耍流氓-_-)
關注 {我},享受文章首發體驗!
每周重點攻克一個前端技術難點。更多精彩前端內容私信 我 回復“教程”
原文鏈接:http://eux.baidu.com/blog/fe/Chrome%E7%9A%84First%20Paint
作者:洪閏輝