對瀏覽器首次渲染時間點的探究

使用 Chrome Devtool 進行性能分析時,在 Performance 面板上,能夠看到用綠線標出來的 First-Contentful-Paint。瀏覽器什麼時候進行首次渲染?網上只能查到一些模棱兩可的資料,今天咱們來探究這個問題。javascript

注: 原始連接: www.404forest.com/2019/04/23/…html

文章備份: github.com/jin5354/404…html5

1. 引子

1.1 術語堪明

  • 首次渲染
  • 首屏時間/首屏渲染

在掘金上用『首次渲染』進行搜索,查不到什麼相關資料;使用『首屏時間』進行搜索,能搜出大量性能優化的文章。點進去看能夠發現,你們常談的『首屏時間』是一個業務概念,指的是業務的首屏內容所有渲染完畢的時間點,通常使用埋點進行手動上報。本文探索的則是瀏覽器進行首次渲染的時間點,此時可能只渲染出了網頁的部份內容。java

first-paint4

瀏覽器什麼時候進行首次渲染?

1.2 提出場景

舉例說明:git

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>demo</title>
</head>
<body>
<div id="app">
<p>俺是用來測試首屏渲染的文字。</p>
</div>
<script src="./bundle.js"></script>
</body>
</html>
複製代碼

這是一個最多見的單頁應用形態。bundle.js 下載完後,執行,構建 DOM 樹,替換 div#app 節點,渲染應用。那麼問題來了,這段用來測試首屏渲染的文字,會不會被渲染到屏幕上?查詢已有的資料,主要從兩個方面講解:github

  • 瀏覽器解析頁面流程:
  1. 解析 HTML,構建 DOM 樹
  2. 解析 CSS,構建 CSSOM
  3. 合併 DOM 和 CSSOM,構建渲染樹(render tree)
  4. 對渲染樹進行佈局,獲得每一個節點的位置、尺寸信息
  5. 對渲染樹進行繪製。

因爲腳本是阻塞 html 解析的,只有下載、執行完,html 解析才宣告結束,此時構建的渲染樹是徹底的,但也已經再也不有測試文字節點了。而在腳本下載、執行完以前,這個『不完整的渲染樹』會渲染嗎?得不出確切的結論。web

  • 『須要着重指出的是,這是一個漸進的過程。爲達到更好的用戶體驗,呈現引擎會力求儘快將內容顯示在屏幕上。它沒必要等到整個 HTML 文檔解析完畢以後,就會開始構建呈現樹和設置佈局。在不斷接收和處理來自網絡的其他內容的同時,呈現引擎會將部份內容解析並顯示出來。』 - 來自『瀏覽器的工做原理:新式網絡瀏覽器幕後揭祕』

這篇講解瀏覽器工做內幕的經典文章表示:HTML 解析完畢以前,也是能夠進行繪製的,那麼測試文字必定就能繪製出來麼?依然沒有明確的答案,感受像是瀏覽器的黑箱。沒有辦法啦,只能本身去儘可能檢索了。chrome

2. 規範解讀

2.1 stage1: paint timing 規範

在網上檢索『首次渲染』、『when does browser first paint』找不到相關的資料。在搜索時,忽然發現一個新的 API PerformancePaintTiming,能夠經過 first-paintfirst-contentful-paint 這兩個 entry name 來獲取首次渲染的時間。趕快去查閱它的規範canvas

4.1.1. Mark paint timingsegmentfault

Perform the following steps:

  1. Let paint-timestamp be the input timestamp.
  1. If this instance of update the rendering is the first paint, then record the timestamp as paint-timestamp and invoke the §4.1.2 Report paint timing algorithm with two arguments: "first-paint" and paint-timestamp.

NOTE: First paint excludes the default background paint, but includes non-default background paint.(這裏能夠發現,默認的白屏不算 first-paint,至少得設個背景色)

  1. Otherwise, if this instance of update the rendering is the first contentful paint, then record the timestamp as paint-timestamp and invoke the §4.1.2 Report paint timing algorithm with two arguments: "first-contentful-paint" and paint-timestamp.

NOTE: This paint must include text, image (including background images), non-white canvas or SVG.(寫了字,放了圖片,就算 first-contentful-paint 啦)

翻譯:若是 update the rendering 實例是 first-paint 那麼就記錄時間戳,上報爲 first-paint 時間。若是 update the rendering 實例是 first-contentful-paint 那麼就記錄時間戳,上報爲 first-contentful-paint 時間。

[update the rendering]((html.spec.whatwg.org/multipage/w…)是啥?點進去,規範直接跳到了 eventloop。恍然大悟,update the rendering 不就是 eventloop 中的最後一個階段嗎!

eventloop-1

不熟悉 eventloop 的同窗可查閱以前文章:[深刻探究 eventloop 與瀏覽器渲染的時序問題](https://www.404forest.com/2017/07/18/how-javascript-actually-works-eventloop-and-uirendering/)

原來瀏覽器對於首次渲染根本就沒有什麼『黑箱操做』,人家只是老老實實的按照 `eventloop` 來運行而已。`eventloop` 第一次進行到 `update the rendering` 階段的時間點那就是 `first-paint` 的時間點了。因而咱們下一步來研究,HTML 解析過程當中,`eventloop` 是怎麼運行的?

2.2 stage2: eventloop 規範

咱們知道 eventloop 按照 task > microtask > render 的順序執行。查閱規範中關於 task 的定義,得:

The HTML parser tokenizing one or more bytes, and then processing any resulting tokens, is typically a task.

HTML 解析是一個典型的 tasktask 執行完才能 render,正如 HTML 解析完才能渲染,很合理。然而經典文章說了,明明能夠邊解析邊繪製的,事情確定不會這麼簡單。

2.3 stage3: html parser 規範

html parser 規範中檢索 eventloop 得:

(原文很晦澀,這裏爲了方便理解,直接翻譯最核心的幾句:)

當解析到 </script> 時:

若是當前文檔存在阻礙 JS 執行的 CSS 或者當前的腳本 不處於 ready to be parser-executed 狀態,spin the event loop,直到再也不存在阻礙 JS 執行的 CSS 且該段腳本處於 ready to be parser-executed

咱們已經知道 CSS 的加載是會阻礙 JS 執行的。而腳本不處於這個 ready to be parser-executed 狀態簡單理解就是還沒下載完。若是出現這兩種狀況,腳本就沒法馬上執行,須要等待。此時要進行 spin the eventloop,查閱規範,該操做即爲:

(簡單翻譯)

  1. 暫存此時正在執行的 task 或 microtask
  2. 暫存此時的 js 執行上下文堆棧
  3. 清空 js執行上下文堆棧
  4. 若是當前正在執行的是 task,執行 microtask checkpoint
  5. 中止執行當前的 task/microtask。繼續執行 eventloop 的主流程。
  6. 當知足條件時,從新添加以前暫存的 task/microtask,恢復暫存的 js 執行上下文堆棧,繼續執行。

簡單的說就是讓 eventloop 中斷並暫存當前正在執行的 task/microtask,保持 eventloop 的繼續執行,待一段時間以後知足條件了再恢復以前的 task/microtask。

那麼問題就水落石出了:

若是在 HTML 解析過程當中,『解析到了某個腳本,但這個腳本被 CSS 阻塞住了或者還沒下載完』,則會中斷暫存當前的解析 task,繼續執行 eventloop,網頁被渲染

若是 JS 所有是內聯的,或者網速好,在解析到</script>時腳本全都已下載完了,則解析 task 不會被中斷,也就不會出現渲染狀況了。

3. 實戰測試

對於 1.2 中的例子,咱們禁用緩存,使用 chrome 模擬 3G 網速,測試結果:

first-paint1

在 bundle.js 加載以前,測試文字被渲染出來了

first-paint2

`First-Contentful-Paint`在很早的位置

可驗證以前的結論:HTML 解析過程當中遇到腳本且腳本處於等待執行狀態(被CSS阻塞/沒下載完),解析中斷,進行渲染。咱們開啓緩存,不限速,讓 bundle.js 走強緩存,瞬間加載:

first-paint3

`First-Contentful-Paint`在 HTML 解析以後

此時解析 Task 不被中斷,渲染只能等到 HTML 解析完成以後再執行啦。

4. 題外話

筆者弄清該問題,花了一兩個小時,寫這篇文章又花了仨小時,查了很多資料,仍是小有收穫的,好比骨架屏的原理就是在解析中斷時提前渲染頁面,順帶鞏固了 eventloop 和瀏覽器渲染機制。在 sf 上看到了有人跟我有一樣的問題:

first-paint5

哇,遇到一樣的探索者真可貴!本是開心的準備迎接知識的海洋,而後:

first-paint6

first-paint8

first-paint9

5. 參考資料

  1. 瀏覽器的工做原理:新式網絡瀏覽器幕後揭祕
  2. PerformancePaintTiming - Web APIs | MDN
  3. Paint Timing 1 - Editor’s Draft, 24 January 2019
  4. update-the-rendering
  5. 深刻探究 eventloop 與瀏覽器渲染的時序問題
  6. concept-task
  7. html-parser
  8. spin-the-event-loop
  9. 瀏覽器首次渲染頁面的時間點是?
相關文章
相關標籤/搜索