使用 Chrome Devtool 進行性能分析時,在 Performance 面板上,能夠看到用綠線標出來的 First-Contentful-Paint
。瀏覽器什麼時候進行首次渲染?網上只能查到一些模棱兩可的資料,今天咱們來探究這個問題。javascript
注: 原始連接: www.404forest.com/2019/04/23/…html
文章備份: github.com/jin5354/404…html5
在掘金上用『首次渲染』進行搜索,查不到什麼相關資料;使用『首屏時間』進行搜索,能搜出大量性能優化的文章。點進去看能夠發現,你們常談的『首屏時間』是一個業務概念,指的是業務的首屏內容所有渲染完畢的時間點,通常使用埋點進行手動上報。本文探索的則是瀏覽器進行首次渲染的時間點,此時可能只渲染出了網頁的部份內容。java
舉例說明: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
因爲腳本是阻塞 html 解析的,只有下載、執行完,html 解析才宣告結束,此時構建的渲染樹是徹底的,但也已經再也不有測試文字節點了。而在腳本下載、執行完以前,這個『不完整的渲染樹』會渲染嗎?得不出確切的結論。web
這篇講解瀏覽器工做內幕的經典文章表示:HTML 解析完畢以前,也是能夠進行繪製的,那麼測試文字必定就能繪製出來麼?依然沒有明確的答案,感受像是瀏覽器的黑箱。沒有辦法啦,只能本身去儘可能檢索了。chrome
在網上檢索『首次渲染』、『when does browser first paint』找不到相關的資料。在搜索時,忽然發現一個新的 API PerformancePaintTiming,能夠經過 first-paint
和 first-contentful-paint
這兩個 entry name
來獲取首次渲染的時間。趕快去查閱它的規範:canvas
4.1.1. Mark paint timingsegmentfault
Perform the following steps:
- Let
paint-timestamp
be the input timestamp.
- 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"
andpaint-timestamp
.
NOTE: First paint excludes the default background paint, but includes non-default background paint.(這裏能夠發現,默認的白屏不算 first-paint,至少得設個背景色)
- 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"
andpaint-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
按照 task > microtask > render
的順序執行。查閱規範中關於 task
的定義,得:
The HTML parser tokenizing one or more bytes, and then processing any resulting tokens, is typically a task.
HTML 解析是一個典型的 task
。task
執行完才能 render
,正如 HTML 解析完才能渲染,很合理。然而經典文章說了,明明能夠邊解析邊繪製的,事情確定不會這麼簡單。
在 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,查閱規範,該操做即爲:
(簡單翻譯)
- 暫存此時正在執行的 task 或 microtask
- 暫存此時的 js 執行上下文堆棧
- 清空 js執行上下文堆棧
- 若是當前正在執行的是 task,執行 microtask checkpoint
- 中止執行當前的 task/microtask。繼續執行 eventloop 的主流程。
- 當知足條件時,從新添加以前暫存的 task/microtask,恢復暫存的 js 執行上下文堆棧,繼續執行。
簡單的說就是讓 eventloop
中斷並暫存當前正在執行的 task/microtask,保持 eventloop
的繼續執行,待一段時間以後知足條件了再恢復以前的 task/microtask。
那麼問題就水落石出了:
若是在 HTML 解析過程當中,『解析到了某個腳本,但這個腳本被 CSS 阻塞住了或者還沒下載完』,則會中斷暫存當前的解析 task
,繼續執行 eventloop
,網頁被渲染。
若是 JS 所有是內聯的,或者網速好,在解析到</script>
時腳本全都已下載完了,則解析 task 不會被中斷,也就不會出現渲染狀況了。
對於 1.2 中的例子,咱們禁用緩存,使用 chrome 模擬 3G 網速,測試結果:
可驗證以前的結論:HTML 解析過程當中遇到腳本且腳本處於等待執行狀態(被CSS阻塞/沒下載完),解析中斷,進行渲染。咱們開啓緩存,不限速,讓 bundle.js 走強緩存,瞬間加載:
此時解析 Task 不被中斷,渲染只能等到 HTML 解析完成以後再執行啦。
筆者弄清該問題,花了一兩個小時,寫這篇文章又花了仨小時,查了很多資料,仍是小有收穫的,好比骨架屏的原理就是在解析中斷時提前渲染頁面,順帶鞏固了 eventloop 和瀏覽器渲染機制。在 sf 上看到了有人跟我有一樣的問題:
哇,遇到一樣的探索者真可貴!本是開心的準備迎接知識的海洋,而後: