如何统计首屏渲染时间

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

认识几个概念

1. First Paint(FP)node

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

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):github

FCP定义的是从页面加载到屏幕上首次有渲染内容的过程,这里的内容能够是文本、图像、svg元素和非白色canvas元素。在下图加载时间线中,图二是FCP的时间点: image.png 咱们用FCP time来表达内容首次渲染时间。如何计算呢?web

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 timecanvas

3. First Meaningful Paint(FMP)api

FMP定义的是从页面开始加载到渲染出主要内容的过程,这个“主要内容”的定义依赖于各浏览器中的实现细节,所以它并无做为一个标准化的指标。在Chrome的Lighthouse面板中咱们能够看到这个指标: image.png浏览器

4. Largest Contentful Paint(LCP)markdown

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

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:responseEndstartTime的差值;
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:上面代码中咱们监听了全部资源的请求,如何取出首屏的图片资源请求?

  1. 对于img标签的图片资源,咱们能够在MutationObserver或者IntersectionObserver监听器中直接操做dom读取imgsrc或者data-src属性,把图片URL保存起来;
  2. 针对背景图片,咱们使用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~

参考:

相关文章
相关标签/搜索