Open lostvita opened 3 years ago
1. First Paint(FP)
First Paint的定义是渲染树首次转变为屏幕像素的过程,我们用FP time来表达首次渲染时间。在FP之前我们看见的屏幕是空白的,那么FP time也可理解为白屏时间。如何计算呢?
First Paint
FP time
FP
if (window.performance) { let pf = window.performance; let pfEntries = pf.getEntriesByType('paint') let fp = pfEntries.find(each => each.name === 'first-paint') console.log('first paint time: ', fp && fp.startTime) }
2. First Contentful Paint(FCP):
FCP定义的是从页面加载到屏幕上首次有渲染内容的过程,这里的内容可以是文本、图像、svg元素和非白色canvas元素。在下图加载时间线中,图二是FCP的时间点: 我们用FCP time来表达内容首次渲染时间。如何计算呢?
FCP
svg
canvas
FCP time
if (window.performance) { let pf = window.performance; let pfEntries = pf.getEntriesByType('paint') let fp = pfEntries.find(each => each.name === 'first-contentful-paint') console.log('first paint time: ', fp && fp.startTime) }
需要区别于FP,总有FP time ≤ FCP time。
3. First Meaningful Paint(FMP)
FMP定义的是从页面开始加载到渲染出主要内容的过程,这个“主要内容”的定义依赖于各浏览器中的实现细节,因此它并没有作为一个标准化的指标。在Chrome的Lighthouse面板中我们可以看到这个指标:
FMP
4. Largest Contentful Paint(LCP)
FMP的范围不好界定,但LCP的范围是恒定的,它定义的是页面开始加载到渲染出(视口内)最大内容(文本或图像等)的过程。如下图加载时间线:
LCP
第一个示例中,Instagram logo是视口中的最大内容,第二个示例中,绿色的文本是视口中的最大内容块。我们用LCP time表达最大内容渲染时间,如何计算呢?
LCP time
new PerformanceObserver(list => { let entries = list.getEntriesByType('largest-contentful-paint'); entries.forEach(item => { console.log('largest contentful pain time: ', item.startTime) }) }).observe({ entryTypes: ['largest-contentful-paint'] });
我们这里定义的首屏是指页面无滚动的情况下,从开始加载到视窗第一屏内容渲染完成的过程,遵循上面几个概念的定义,我们可以称它为 last contentful paint,亦或first screen paint更贴切一些。在本文,我们就把首屏渲染时间叫做first screen paint time(FSP time),要如何来统计呢?
last contentful paint
first screen paint
(FSP time
先考虑最简单的场景:我们的页面是纯静态文本型的,即首屏里面没有图片,内容是静态文本。
我们要先解决一个问题:如何界定哪些元素是属于屏内的?
1. getBoundingClientRect
getBoundingClientRect用于获取某个元素相对于视窗的位置,理论上我们只要计算每一个元素的位置信息,结合视窗的高度信息,我们就能判断元素是否属于屏内。
getBoundingClientRect
但在真实情况下,一个页面dom的数量是很庞大的,大量的dom操作本身就会影响整个页面的性能!何况,getBoundingClientRect会引起页面重排(what forces reflow/layout),这并不是一个理想的方案;
2. IntersectionObserver + MutationObserver
IntersectionObserver通过启动一个观察器,以一种异步的方式检查目标元素是否出现于视窗(viewport)中,它返回的数据里面包含了两个重要的信息:
IntersectionObserver
接下来我们需要给每一个元素添加一个intersection观察器,MutationObserver可以帮助我们,它提供了监视dom树变更的能力,我们使用它监视document根节点的子树的变化,为新增的每一个子节点注册一个IntersectionObserver,参考如下代码:
MutationObserver
document
// 注册可视性监听器 const isObserver = new IntersectionObserver((entries) => { entries.forEach((entry) => { // 屏内元素 if (entry.intersectionRatio > 0) { // 记录节点及其时间,这里也可以使用人工打点的方式:performance.now() console.log(`${entry.target}: ${entry.time}`); } }); }); // 注册DOM树变更监听器 const muObserver = new MutationObserver((mutations) => { if (!mutations) return; mutations.forEach((mu) => { if (!mu.addedNodes || !mu.addedNodes.length) return; mu.addedNodes.forEach((ele) => { // 只对元素节点进行监听 if (ele.nodeType === 1) { // 添加可视性变化监听器 isObserver.observe(ele); } }); }); }); // 监听document的子树变化 muObserver.observe(document, { childList: true, subtree: true });
更完整的代码参考:first-screen-paint
场景2:首屏包含图片资源,可能是图片元素或背景,需要计算加载最慢那张图片资源的耗时
问题1:图片资源是异步加载的,如何获取资源的请求耗时?
前文我们介绍了获取LCP time的方法,用类似的方式,我们也能获取图片资源的耗时,使用PerformanceObserver api监听资源的加载耗时,它返回的数据里面包含了几个重要的信息:
PerformanceObserver
responseEnd
startTime
const pfObserver = new PerformanceObserver((list) => { const entries = list.getEntriesByType('resource'); entries.forEach((item) => { // 各种资源的耗时 // 首屏图片资源白名单:imgUrlWhiteList = [] console.log(`${item.name: ${item.duration}}`); }); }); // 设定性能监听类别:资源 pfObserver.observe({ entryTypes: ['resource'] });
问题2:上面代码中我们监听了所有资源的请求,如何取出首屏的图片资源请求?
img
src
data-src
background-image
场景3:首屏内容是动态fetch的,甚至fetch的是图片资源,就如商城首页?
数据是动态fetch的,如果是纯文本数据,无图片资源。我们的DOM树变更监听器可以监听到数据返回之后的渲染情况,渲染过程会收集这些节点的可见性变化时间(这个时间肯定是在fetch数据返回时间点之后的);如果渲染的是图片资源,那么就进入了上一个处理图片资源的场景。
1. 首屏内容还在加载中,用户触发了页面滚动?
页面滚动之后,第二屏的内容就会出现在视窗,原本属于首屏的内容(部分内容可能并未完成渲染)却没在视窗中。那么,按照如上的统计方式,就会统计到当前处于视窗内容的渲染时间,这可能就是一个“误差”。
我们需要一个共识:在首屏内容完全渲染之前页面触发了滚动,说明页面已经是一个可交互的状态,这种情况下,我们认为,用户触发滚动时那一帧的内容,已经是用户和开发者双方都能接受的首屏内容。基于这个前提,我们的处理方式是:
在页面滚动时,加一个锁,停止监听后续内容的变更,以初次滚动的时间点为时间界线,统计在此时间点前发出的(依据startTime)所有资源的请求耗时和dom树节点的渲染时间;
2. 在场景3下,首屏内容未加载完,用户触发了页面滚动?
在本测试demo中,页面的主体内容是img元素,按照LCP(lagest contentful paint)的定义,LCP time会返回这张图片渲染的时间;而我们的首屏内容亦是这张图片,那么我们的FSP time应该基本等于LCP time,在下面截图中,也基本验证了这一点!
FSP time
最后,对于上面提到的几个问题,各位读者有任何看法也可在评论区留言~
仓库地址:first-screen-paint,欢迎提issue~
参考:
认识几个概念
1. First Paint(FP)
First Paint
的定义是渲染树首次转变为屏幕像素的过程,我们用FP time
来表达首次渲染时间。在FP
之前我们看见的屏幕是空白的,那么FP time
也可理解为白屏时间。如何计算呢?2. First Contentful Paint(FCP):
FCP
定义的是从页面加载到屏幕上首次有渲染内容的过程,这里的内容可以是文本、图像、svg
元素和非白色canvas
元素。在下图加载时间线中,图二是FCP
的时间点:FCP time
来表达内容首次渲染时间。如何计算呢?需要区别于FP,总有FP time ≤ FCP time。
3. First Meaningful Paint(FMP)
FMP
定义的是从页面开始加载到渲染出主要内容的过程,这个“主要内容”的定义依赖于各浏览器中的实现细节,因此它并没有作为一个标准化的指标。在Chrome的Lighthouse面板中我们可以看到这个指标:4. Largest Contentful Paint(LCP)
FMP
的范围不好界定,但LCP
的范围是恒定的,它定义的是页面开始加载到渲染出(视口内)最大内容(文本或图像等)的过程。如下图加载时间线:LCP time
表达最大内容渲染时间,如何计算呢?什么是首屏渲染?
我们这里定义的首屏是指页面无滚动的情况下,从开始加载到视窗第一屏内容渲染完成的过程,遵循上面几个概念的定义,我们可以称它为
last contentful paint
,亦或first screen paint
更贴切一些。在本文,我们就把首屏渲染时间叫做first screen paint time(FSP time
),要如何来统计呢?统计首屏渲染时间
我们要先解决一个问题:如何界定哪些元素是属于屏内的?
1. getBoundingClientRect
getBoundingClientRect
用于获取某个元素相对于视窗的位置,理论上我们只要计算每一个元素的位置信息,结合视窗的高度信息,我们就能判断元素是否属于屏内。但在真实情况下,一个页面dom的数量是很庞大的,大量的dom操作本身就会影响整个页面的性能!何况,
getBoundingClientRect
会引起页面重排(what forces reflow/layout),这并不是一个理想的方案;2. IntersectionObserver + MutationObserver
IntersectionObserver
通过启动一个观察器,以一种异步的方式检查目标元素是否出现于视窗(viewport)中,它返回的数据里面包含了两个重要的信息:接下来我们需要给每一个元素添加一个intersection观察器,
MutationObserver
可以帮助我们,它提供了监视dom树变更的能力,我们使用它监视document
根节点的子树的变化,为新增的每一个子节点注册一个IntersectionObserver
,参考如下代码:更完整的代码参考:first-screen-paint
问题1:图片资源是异步加载的,如何获取资源的请求耗时?
前文我们介绍了获取LCP time的方法,用类似的方式,我们也能获取图片资源的耗时,使用
PerformanceObserver
api监听资源的加载耗时,它返回的数据里面包含了几个重要的信息:responseEnd
于startTime
的差值;问题2:上面代码中我们监听了所有资源的请求,如何取出首屏的图片资源请求?
MutationObserver
或者IntersectionObserver
监听器中直接操作dom读取img
的src
或者data-src
属性,把图片URL保存起来;background-image
的值;数据是动态fetch的,如果是纯文本数据,无图片资源。我们的DOM树变更监听器可以监听到数据返回之后的渲染情况,渲染过程会收集这些节点的可见性变化时间(这个时间肯定是在fetch数据返回时间点之后的);如果渲染的是图片资源,那么就进入了上一个处理图片资源的场景。
两个问题
1. 首屏内容还在加载中,用户触发了页面滚动?
页面滚动之后,第二屏的内容就会出现在视窗,原本属于首屏的内容(部分内容可能并未完成渲染)却没在视窗中。那么,按照如上的统计方式,就会统计到当前处于视窗内容的渲染时间,这可能就是一个“误差”。
我们需要一个共识:在首屏内容完全渲染之前页面触发了滚动,说明页面已经是一个可交互的状态,这种情况下,我们认为,用户触发滚动时那一帧的内容,已经是用户和开发者双方都能接受的首屏内容。基于这个前提,我们的处理方式是:
在页面滚动时,加一个锁,停止监听后续内容的变更,以初次滚动的时间点为时间界线,统计在此时间点前发出的(依据
startTime
)所有资源的请求耗时和dom树节点的渲染时间;2. 在场景3下,首屏内容未加载完,用户触发了页面滚动?
测试一下
在本测试demo中,页面的主体内容是img元素,按照![image](https://user-images.githubusercontent.com/26505680/118442162-cfe83100-b71c-11eb-9fa0-8b724ab3e623.png)
LCP
(lagest contentful paint)的定义,LCP time
会返回这张图片渲染的时间;而我们的首屏内容亦是这张图片,那么我们的FSP time
应该基本等于LCP time
,在下面截图中,也基本验证了这一点!最后,对于上面提到的几个问题,各位读者有任何看法也可在评论区留言~
仓库地址:first-screen-paint,欢迎提issue~
参考: