说到首屏时间,很多人就会觉得不就是FP\FCP之类的性能指标嘛?但是我们要知道在前端统计的性能指标中,首屏时间是一个极为“特殊”的指标。 它的特殊性体现在他的计算规则的不统一,而计算规则的不统一是因为各自对“首屏”的概念不统一。就比如说:我们的首次渲染的页面存在图片的加载,那么我们的首屏时间的计算是大部分dom都渲染完成而不等待图片加载,还是要一直等在这张图片加载完成才算我们定义的首屏概念呢?
背景
前端页面性能的监控主要分为两种,一种是合成监控,另一种是真实用户监控。其中,合成监控就是指在一个模拟场景中提交一个需要做性能审计的页面,按照一定的规则去运行页面来提取一些性能指标得到一份性能审计报告,例如Lighthouse。而真实监控是指我们通过监控工具做的上报、清洗、加工最终得到的性能数据。 而在前端性能监控中其中一个非常重要的指标 - 首屏时间。
一、首屏时间定义
共性定义:首屏时间是指用户打开网站开始,到展示首屏(第一屏)内容渲染完成的时间。
个性定义:这里大致可分分为SSR渲染和SPA渲染两种方式。
其中SSR渲染中认为首屏时间是指从加载资源到html中的body渲染完成,这阶段所耗费的时间就是首屏时间。而对于SPA来说,在vue、react之前的首屏时间也是计算到body渲染完成截止,但随着前端框架的出现,通常是通过js操作dom的方式来给挂载节点进行相关dom渲染。而框架打包后对应的script标签的加载方式就会影响到首屏渲染结束时间。如果是同步加载的话可以通过domcontentload,如果是异步加载的话(async、defer),其中defer是不受影响的,因为domcontentlload是一定会等到defer执行完毕后在触发的。但是如果是异步加载,那么当body解析完毕时,async还在远程加载中,那么就会在async加载完毕之前触发comcontentload事件(否则在body解析完毕之前加载完成那么就会依然是domcontentload最后触发,因为async是加载完毕后立即执行)。此外,还有一个点也能说明domcontent不准。因为前端中有很多是api接口请求数据,那么即使前端渲染好之后,但是接口数据还在等在响应中,那么用户看到的都是空值,因此也不能严格意义上算首屏时间结束,应该是等到初始的dom架构渲染好,并且正确的数据也返回渲染好,这个时候才是首屏结束。
二、首屏时间计算规则
- 首屏时间 = 首屏内容渲染结束时间点 - 开始请求的时间点
首屏时间计算规则 | 存在问题 |
---|---|
规则1: FCP时间作为首屏时间 | |
规则2: DOMContentLoaded 事件来表示首屏时间 | 现在前端项目复杂性的提升,DOMContentLoaded 已经不能很好的描述首屏渲染时间,因为它的触发时机是不确定的,会受到 JS 脚本的影响,而且有很多种触发的情况需要考虑,这种计算规则通常通过performanc来实现,但是随着框架的出现,准确性大大丧失。参考juejin.cn/post/721483… |
规则3: vue框架mounted挂载的时间 | 在App.vue的mounted生命周期里计算时间,但mounted执行并不代表首屏所有元素加载完毕,所以mounted计算出来的时间会偏短。 |
规则4: vue框架mounted挂载的时间 | 计算出来的时间会偏短: 因为在App.vue的mounted生命周期里计算时间,但mounted执行并不代表首屏所有元素加载完毕。如果接口的响应时间慢,vue虽然挂载了,但是里面只有初始数据,而不是真实数据,这严格意义上可能都不算首屏渲染完成 |
规则5: nextTick回调的时候计算 | 计算出来的时间会偏长:nextTick回调的时候,首屏的DOM都渲染出来了,但是计算首屏时间并不需要渲染所有DOM |
规则6: 通过首屏有意义时间计算 | 这种一般很难清晰地界定哪些元素的加载是「有用」的(因此目前尚无规范),但对于开发者他们自己而言,他们更知道页面的哪些部分对于用户而言是最为有用的,所以这样的衡量标准更多的时候是掌握在开发者手上! |
三、首屏时间计算方式
3.1 方式概览
-
performance计算:随着 Vue 和 React 等前端框架盛行,
Performance
已无法准确的监控到页面的首屏时间,因为DOMContentLoaded
的值只能表示空白页(当前页面 body 标签里面没有内容)加载花费的时间。浏览器需要先加载 JS , 然后再通过 JS 来渲染页面内容,这个时候单页面类型首屏才算渲染完成 -
用户自定义打点—最准确的方式(只有用户自己最清楚,什么样的时间才算是首屏加载完成)
- 缺点:侵入业务,成本高
-
粗略的计算首屏时间:
loadEventEnd - fetchStart/startTime
或者domInteractive - fetchStart/startTime
-
通过计算首屏区域内的所有图片加载时间,然后取其最大值
-
利用 MutationObserver 接口,监听 document 对象的节点变化
3.2 FMP - MutationObserve计算方式
我们需要利用MutationObserver
监控DOM的变化,监控每一次DOM变化的分数,计算的规则为: (1 + 层数 * 0.5)
<body>
<div>
<div>1</div>
<div>2</div>
</div>
</body>
对应dom结构如图所示,所对应的dom分数:1.5 + 2 + 2.5 + 2.5 = 8.5(分)
目前来说,业界通用的计算首屏时间的思路主要是通过FMP来认为是首屏的时间,具体步骤如下:
- step1:通过MutationObserver来监听body元素下面的增删改操作,每次监听之后执行对应回调;
- step2:每次执行回调的时候计算当前dom结构的系数,以及对应时间戳,一并放在统一的dom-stash数组中,如下所示;
let observer = new MutationObserver(() => {
// 计算每次DOM修改时,距离页面刚开始加载的时间
const start = window.performance.timing.navigationStart
const time = new Date().getTime() - start
const body = document.querySelector('body')
const score = computedScore(body, 1)
// 加到数组 observerData 中
observerData.push({
score,
time
})
})
observer.observe(
document, {
childList: true,
subtree: true
}
)
- step3:然后分析stash数组中每个元素对应的dom-staths系数,找到变化率最大的两个dom系数,那么就可以认为是FMP对应的截止时间。
- step4:通过FMP截止时间 - fetchStart时间得到首次有意义渲染时间,这里认为是首屏时间。
这里我认为需要解释的点:
问题1:dom结构系数计算公式如何得到?
这里计算系数(1 + 层数 * 0.5)中权重0.5其实不重要,我们也可以改成0.6、0.7、0.8,起作用是为了将每个节点对应的dom值通过一定计算来区分中所在层级。例如第三层某个dom节点是(1 + 3 * 0.5),那么我们减去最外层body的dom值(1 + 1 * 0.5),得到1,那么我们就知道这个dom节点与body之间间隔了1/0.5=2层。这样我们就能清楚的通过系数值知道每个节点对应所在的dom结构。
问题2:为什么要计算dom结构系数?
我们在知道计算dom系数可以一定程度上表现出dom内部结构的复杂性(数量上、层级深度上),所以我们要有一套指标来衡量dom结构的复杂程度。
问题3:为什么要将dom系数增长率最大的点作为FMP计算的截止点?而不是以最后的那个dom加载完成(即最大的dom系数)的点作为FMP计算的终止节点?
此外,为什么我们要算的是FMP?我们现在把这个问题反过来讲,如果我们不算FMP,即不以增长率最大的作为终止时间,而是以最后dom节点(一定是最大的dom系数值)作为增长时间。那么可能就会出现一些情况,例如:我们在最后通过异步加载某个li列表item需要很长的时间,但是这个li-item在整个页面中并不是特别重要,那么我们以最后完成dom时间作为首屏时间就会有失偏颇。因此我们需要的是对用户来说有主要意义的dom结构才能认为是首屏有意义内容,而目前dom增长率是最接近这种要求的衡量指标。
但是在存在一些计算失效的情况,例如:增长率都很大的几段,那么这个时候只单独的找最大其实也有失偏颇。这个时候可以按照组大的dom系数值来作为截止时间。
总之,目前FMP的计算方式尚无统一定调,还需要具体问题具体分析。
这里先要区分是SPA还是SSR渲染
SSR首屏时间:首屏时间的标志是ssr使用的dom渲染结束
目前达成共识的首屏定义现状:现在 "首屏渲染时间" 的计算大多数时候是依靠人工计算。
三、常见计算方式
参考:
juejin.cn/post/738721…
juejin.cn/post/703564…
juejin.cn/post/714017…