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






