Chrome的First Paint觸發的時機探究

前言

First paint 直譯過來的意思就是瀏覽器第一次渲染(paint),在First paint以前是白屏,在這個時間點以後用戶就能看到(部分)頁面內容。css

因此研究這個First Paint的觸發時機對於優化瀏覽器頁面的首屏渲染時間有很重要的做用。html

在正題開始以前,先說下瀏覽器的頁面的加載流程(大致過程是這樣,並不精確,只是爲了幫助理解後面內容):vue

  1. 瀏覽器輸入url,瀏覽器發送請求到服務器,服務器將請求的HTML返回給瀏覽器。
  2. 瀏覽器下載完成HTML(Finish Loading HTML)以後,便開始從上到下解析。
  3. 解析的過程當中碰到css和js外鏈(其實HTML的下載也是這個流程)都會執行如下過程:
    1. Send Request:表示給這個外鏈對應的服務器發送請求
    2. Receive Response: 表示接收響應,這裏是表示告訴瀏覽器能夠開始從網絡接收數據了
    3. Receive Data:表示開始接收數據
    4. Finish Loading: 表示已經完成下載數據。
    5. Parse Stylesheet/Evaluate(默認狀況下js下載完成以後執行Evaluate,css下載完成後會進行Parse Stylesheet
  4. 全部的css下載完成後Parse Stylesheet而後開始構建CSSOM
  5. DOM(文檔對象模型)和 CSSOM(CSS對象模型)會合並生成一個渲染樹(Render Tree)
  6. 根據渲染樹的內容計算處各個節點在網頁中的大小和位置(Layout,能夠理解爲「刻章」)
  7. 根據Layout繪製內容在瀏覽器上(Paint,能夠理解爲「蓋章」)。

正題開始

在最新版的Chrome的perfomance中是能直接看到First Paint這個時間點的,爲了方便你們測試,我就直接拿谷歌這個示例頁面來作演示:react

測試頁面jquery

用chrome打開上面連接,最好是隱身模式,防止插件亂入影響判斷,按F12或者右鍵檢查元素打開控制檯先切換到Network選項,勾選禁用緩存(緩存也會影響到判斷):git

1

切換到Perfomance,勾選Screenshots並點擊紅框進行頁面分析(會自動中止的,不用點stop):github

分析完後能夠看到以下結果:web

上圖中的綠色的線就是當前頁面第一次出現內容的時間點,能夠將鼠標放到Main上面的Network中綠色的線附近能夠看到在他以前頁面空白,在他以後就有內容。
除了綠色的線還有藍色以及紅色的線,這裏也解釋一下:chrome

簡單講一下DOMContentLoadedload的區別:bootstrap

  1. DOMContentLoaded是HTML文檔(包括CSS、JS)被加載以及解析完成以後觸發(即 HTML->DOM的過程完成 )
  2. load則是在頁面的其餘資源如圖片、字體、音頻、視頻加載完成以後觸發
  3. load事件通常在DOMContentLoaded以後才觸發(也有可能在它以前哦)

這個時候發現綠色虛線以前有一個淺綠色方塊,相應的解釋以下:

由圖能夠得出「淺綠色」表明的是根據CSSOM計算樣式並進行佈局繪製的過程,這段時間內瀏覽器作了一下事情:

  1. Recalculate Style:從新計算樣式,肯定DOM元素的樣式規則(定規則)
  2. Layout:根據計算結果進行佈局,肯定元素的大小和位置(刻章)
  3. Update Layer Tree: 更新渲染層樹
  4. Paint: 繪製,根據前面的Layer Tree繪製頁面(位置、大小、顏色、邊框、陰影等)(蓋章)
  5. Composite Layers: 造成層,瀏覽器按照合理的順序合併成一個圖層而後輸出到屏幕(給別人看)

那何時開始First paint呢?在淺綠色方塊最前面的虛線往前看,發如今灰色虛線以前都會有一個步驟:就是Parse Stylesheet(調研了不少頁面都是如此)

因此,First Paint的加載流程應該是這樣:

  1. 全部的CSS加載完成
  2. Parse Stylesheet:構建出CSSOM
  3. Recalculate Style:從新計算樣式,肯定DOM元素的樣式規則(定規則)
  4. Layout:根據計算結果進行佈局,肯定元素的大小和位置(刻章)
  5. Update Layer Tree:更新渲染層樹
  6. Paint:繪製,根據前面的Layer Tree繪製頁面(位置、大小、顏色、邊框、陰影等)(蓋章)
  7. 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執行完成以後。換句話說就是第一腳本以前的DOMCSSOM準備就緒以後,便會着手渲染第一腳本前的內容。

可是...你覺得到這裏就結束了?其實沒有。

第十種狀況:

這種狀況中,head中既有JS也有CSS,body中也有第一腳本存在:


注意上圖中的vue.js是在head中的,然後面的JS文件都在body中,並且,vue.js加載完成以後,body中的JS還沒下載完成,這個時候咱們調換一下vue.jsangular2.js的位置:


看,這個時候又沒有提早渲染了,123等到全部JS文件都執行完以後才渲染,這種狀況除了驗證了第九點的結論,還能補充咱們的結論:

若是第一腳本前的JS和CSS加載完了,body中的腳本還未下載完成,那麼瀏覽器就會利用構建好的局部CSSOMDOM提早渲染第一腳本前的內容(觸發FP);若是第一腳本前的JS和CSS都還沒下載完成,body中的腳本就已經下載完了,那麼瀏覽器就會在全部JS腳本都執行完以後才觸發FP。

到這裏本次探究就結束了,其實還有不少種狀況,感興趣的能夠本身去試試。

建議:

  • CSS放在head中,JS放在</body>前(若是在head必須放JS,也儘可能減小他的大小,把大JS文件放</body>前)。
  • 減少head中CSS和JS大小(gzip瞭解一下?),
  • 優化head中的JS和CSS外鏈的網絡狀況,減小StalledTTFBContent Download的時間。
  • 在第一腳本前使用骨架圖,能夠減小用戶的白屏感知時間(對於使用JS插入模板來渲染的框架,建議將骨架圖的路由生成邏輯單獨提出來)

科普一下

  • Chrome會渲染局部CSSOMDOM
  • First PaintDOMContentLoadedload事件的觸發沒有絕對的關係,FP可能在他們以前,也可能在他們以後,這取決於影響他們觸發的因素的各自時間(FP第一腳本CSSOMDOM的構建速度;DOMContentLoadedHTML文檔自身以及HTML文檔中全部JSCSS的加載速度;load:圖片、音頻、視頻、字體的加載速度)。
  • DOMContentLoadedload事件也沒有強制的前後順序,DOMContentLoaded通常在load事件以前觸發,但也可能在load事件以後觸發。
  • 第一腳本前的CSS若是還會去加載字體文件,那麼即便CSSOMDOM構建完成觸發FP,頁面內容也會是空白,只有等到字體文件下載完成纔會出現內容(這也是咱們在打開一個加載了谷歌字體的網站會白屏很長時間的緣由)。
  • 默認狀況下,CSS外鏈之間是誰先加載完成誰先解析,可是JS外鏈之間即便先加載完成,也得按順序執行。
  • link外鏈後面緊跟script外鏈,須先等link parse完成以後,script纔會執行,即便script先下載完成。script後面緊跟link,也是同樣,會等script執行完以後,link纔會parse
  • 若是script以後緊跟幾個linkscript比這幾個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 Requestreceive Response這段時間。
  • 瀏覽器會給HTML中的資源文件進行等級分類(Hightest/High/Meduim/Low/Lowest),通常HTML文檔自身、head中的CSS都是Hightesthead中JS通常是High,而圖片通常是Low,而設置了async/defer的腳本通常是Lowgif圖片通常是Lowest
  • 下圖中的資源文件淺色和深色和第二個圖畫紅框的位置是對應的(不信本身計算一下對應的時間)


參考連接:

  1. 分析關鍵渲染路徑性能
  2. CSS/JS對DOM渲染的影響
  3. CSS Animation性能優化

個人博客即將搬運同步至騰訊雲+社區,邀請你們一同入駐:https://cloud.tencent.com/developer/support-plan?invite_code=3akuvl3965mok

相關文章
相關標籤/搜索