使用 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 上看到了有人跟我有一样的问题:
哇,遇到一样的探索者真可贵!本是开心的准备迎接知识的海洋,而后: