如何统计首屏渲染时间

您所在的位置:网站首页 时间节点指什么 如何统计首屏渲染时间

如何统计首屏渲染时间

2023-04-07 02:48| 来源: 网络整理| 查看: 265

仓库完整代码:first-screen-paint,如果来过,期待留下你的一颗小星星~

认识几个概念

1. First Paint(FP)

First Paint的定义是渲染树首次转变为屏幕像素的过程,我们用FP time来表达首次渲染时间。在FP之前我们看见的屏幕是空白的,那么FP time也可理解为白屏时间。如何计算呢?

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的时间点: image.png 我们用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面板中我们可以看到这个指标: image.png

4. Largest Contentful Paint(LCP)

FMP的范围不好界定,但LCP的范围是恒定的,它定义的是页面开始加载到渲染出(视口内)最大内容(文本或图像等)的过程。如下图加载时间线: image.png

image.png 第一个示例中,Instagram logo是视口中的最大内容,第二个示例中,绿色的文本是视口中的最大内容块。我们用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),要如何来统计呢?

统计首屏渲染时间

先考虑最简单的场景:我们的页面是纯静态文本型的,即首屏里面没有图片,内容是静态文本。

我们要先解决一个问题:如何界定哪些元素是属于屏内的?

1. getBoundingClientRect

getBoundingClientRect用于获取某个元素相对于视窗的位置,理论上我们只要计算每一个元素的位置信息,结合视窗的高度信息,我们就能判断元素是否属于屏内。

但在真实情况下,一个页面dom的数量是很庞大的,大量的dom操作本身就会影响整个页面的性能!何况,getBoundingClientRect会引起页面重排(what forces reflow/layout),这并不是一个理想的方案;

2. IntersectionObserver + MutationObserver

IntersectionObserver通过启动一个观察器,以一种异步的方式检查目标元素是否出现于视窗(viewport)中,它返回的数据里面包含了两个重要的信息:

time:元素可见性发生变化的时间,一个高精度时间戳,单位毫秒; intersectionRatio:目标元素的可见比例,介于0.0-1.0,为0时表示元素不可见,为1时表示元素完全可见。

接下来我们需要给每一个元素添加一个intersection观察器,MutationObserver可以帮助我们,它提供了监视dom树变更的能力,我们使用它监视document根节点的子树的变化,为新增的每一个子节点注册一个IntersectionObserver,参考如下代码:

// 注册可视性监听器 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监听资源的加载耗时,它返回的数据里面包含了几个重要的信息:

name:资源URL; initiatorType:资源类型,取值可能是css|img|xmlhttprequest等; startTime:请求开始时间,高精度时间戳值,单位毫秒; responseEnd:请求响应返回的时间,高精度时间戳值,单位毫秒; duration: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标签的图片资源,我们可以在MutationObserver或者IntersectionObserver监听器中直接操作dom读取img的src或者data-src属性,把图片URL保存起来; 针对背景图片,我们使用getComputedStyle方法获取节点的样式表,并取出其background-image的值;

场景3:首屏内容是动态fetch的,甚至fetch的是图片资源,就如商城首页?

数据是动态fetch的,如果是纯文本数据,无图片资源。我们的DOM树变更监听器可以监听到数据返回之后的渲染情况,渲染过程会收集这些节点的可见性变化时间(这个时间肯定是在fetch数据返回时间点之后的);如果渲染的是图片资源,那么就进入了上一个处理图片资源的场景。

两个问题

1. 首屏内容还在加载中,用户触发了页面滚动?

页面滚动之后,第二屏的内容就会出现在视窗,原本属于首屏的内容(部分内容可能并未完成渲染)却没在视窗中。那么,按照如上的统计方式,就会统计到当前处于视窗内容的渲染时间,这可能就是一个“误差”。

我们需要一个共识:在首屏内容完全渲染之前页面触发了滚动,说明页面已经是一个可交互的状态,这种情况下,我们认为,用户触发滚动时那一帧的内容,已经是用户和开发者双方都能接受的首屏内容。基于这个前提,我们的处理方式是:

在页面滚动时,加一个锁,停止监听后续内容的变更,以初次滚动的时间点为时间界线,统计在此时间点前发出的(依据startTime)所有资源的请求耗时和dom树节点的渲染时间;

2. 在场景3下,首屏内容未加载完,用户触发了页面滚动?

这种情况下只能保底统计到fetch请求的响应结束时间; 如果用户在响应之前触发了滚动,这时候数据渲染尚未开始,我们的程序无法捕捉到dom节点,那么也拿不到响应的图片资源,也就无法统计后续的渲染时间; 如果用户是在数据返回之后,图片资源渲染之前触发的滚动,这种情况下由于能够捕捉dom树节点的渲染,理论上我们也能够获取响应图片资源的加载耗时; 测试一下

在本测试demo中,页面的主体内容是img元素,按照LCP(lagest contentful paint)的定义,LCP time会返回这张图片渲染的时间;而我们的首屏内容亦是这张图片,那么我们的FSP time应该基本等于LCP time,在下面截图中,也基本验证了这一点! image.png

最后,对于上面提到的几个问题,各位读者有任何看法也可在评论区留言~

仓库地址:first-screen-paint,欢迎提issue~

参考:

web.dev/fcp/ web.dev/first-meani… web.dev/lcp/


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3