前言
SPA项目中,首屏加载速度都是老生常谈的问题了,首屏时间直接反应了用户多久能看到页面的主要内容,这决定了用户体验,本文聊一聊如何采集首屏时间,本文主要是单指正常记录首屏时间(不和首屏js资源报错等等挂钩)
Performance
timing
- connectStart:HTTP域名解析完成的时间
- connectEnd:HTTP浏览器与服务器之间连接建立完成的时间
- domComplete:DOM文档解析完成,readyState变为complete
- domContentLoadedEventStart:所有脚本已经执行完,开始执行DOMContentLoaded方法
- domContentLoadedEventEnd:执行DOMContentLoaded方法结束
- domInteractive:DOM结构加载结束,开始加载内嵌资源,readyState变为interactive
- domLoading:DOM结构开始解析,readyState开始是loading
- domainLookupStart:DNS域名查询开始
- domainLookupEnd:DNS域名查询结束
- fetchStart:浏览器发起任何请求之前的时间戳
- loadEventStart:开始加载load事件
- loadEventEnd:load事件加载结束
- navigationStart:unload上一个文档的时间节点
- redirectStart:第一个页面重定向开始的时间
- redirectEnd:最后一个页面重定向结束的时间
- requestStart:浏览器向服务器发起HTTP请求(包含缓存,本地资源)
- responseStart:浏览器从服务器收到HTTP请求返回的第一个字节的时间
- responseEnd:浏览器从服务器收到HTTP请求返回的最后一个字节的时间
- secureConnectionStart:HTTPS协议握手之前的时间,如果非HTTPS,则为0
- unloadEventStart:上一个文档unload事件的开始时间(需要是同源文档,否则为0)
- unloadEventEnd:上一个文档unload事件的结束时间(需要是同源文档,否则为0)
那么首屏的时间是不是可以简单取值为:
domComplete - navigationStart
答案是不可以的,因为在Vue和React等SPA框架中,页面是空的,需要加载js,然后通过js脚本来把页面内容渲染出
来,所以上面简单的运算是得不到真正首屏时间的
自动化采集和思考
手动化采集侵入代码性强,而且也无法一劳永逸,可能也导致数据不够标准,所以这里我采用的方式自动化采集,就是用一段代码来做首屏的自动化采集。这里思考热门方式:
MutationObserver 监听根节点的 dom 节点数量
当然还有个方案,计算计算FMP 如何相对准确的计算 FMP (当然我这里没有使用该文章的方式,因为觉得执行起来太过复杂)
此 API 监听页面 DOM 变化,并告诉我们每次变化的 DOM 是被增加还是删除
MutationObserver缺点: 无法兼容骨架屏有无的情况,如果页面有骨架屏,也没法真正检测出真正的白屏时间
而且难以决定什么是加载页面完成的标记
我的方案
实现:
其实我的思考的方案很简单,核心代码其实只有两行,就是通过 Vue.mixin() 混入组件 mounted 的时间,然后统计每个组件的加载在页面上的时间,最后一个组件加载的时间就是用户看到的首屏时间(因为所有组件已经加载完毕,不包括异步组件)
import Vue from 'vue';
class whiteScreen {
constructor() {
const timing = window.performance.timing;
// 记录开始时间
this.startTime = timing.navigationStart || timing.connectStart ||
dayjs().format('YYYY-MM-DD HH:mm:ss:SSS');
// 加载中状态
this.loading = false;
// 收集每个组件加载的完成数据
this.times = [];
// 记录组件是否加载过,因为一个页面会多次用到某组件,只记录第一次加载成功即可
this.isLoadedComp = {};
// 是否在加载中
this.setLoading(true);
// 利用vue的mixin记录每个组件挂载完成的时间
const _this = this;
Vue.mixin({
mounted() {
// 如果不是正在加载中,则返回
if (!_this.isLoading()) return;
// 获取组件标签
const name = this.$options.name || this.$options._componentTag;
// 如果该组件已经加载过,则不用再记录
if (_this.isCompLoaded(name)) return;
this.$nextTick(() => {
if (name) {
_this.push({
name: name,
// 记录当前组件加载成功的时间
time: dayjs().format('YYYY-MM-DD HH:mm:ss'),
});
}
});
}
});
}
isLoading() {
return this.loading;
}
setLoading(value) {
this.loading = value;
if (!this.isLoading()) {
const data = [...this.times];
const startTime = this.startTime;
// TODO: 上传埋点
console.log({
data,
startTime,
});
}
}
isCompLoaded(name) {
if (!this.isLoadedComp[name]) {
this.isLoadedComp[name] = true;
return false;
}
return this.isLoadedComp[name];
}
}
解释一下上述代码,如上面所说的,用了Vue.mixin的方式记录每个组件的加载的完成时间,上面有个对象 isLoadedComp 用来记录页面是否加载过组件,举个例子说明:
page含有A组件,但是A组件在page有被多次使用到,所以我们只需要第一次加载作为依据即可
使用了这段代码后,我们就会得到这样如下的 data 数据结构,如下图:
这样我们准确的取得了页面加载每个组件用到的时间,但是还存在一个问题,上面的 loading 状态应该何时结束,我们要根据什么作为页面加载完成的依据,这里大家不妨思考下
设置组件最大加载时间
来看看这种情况,页面 page 有异步组件的情况,page有10个组件,2个组件是异步,8个同步组件,加载同步组件需要2面,加载异步组件需要10秒,理论上我们的白屏时间应该2s,而不是8秒,因为此时用户已经能看到界面,并且可以做一些有效点击操作,所以我们结合上面的,什么作为页面加载完成的依据得出我们的设计方式
这样我们可以设置一个组件最大的加载时间,用一个倒计时,每次组件加载完就清空倒计时,再重新创建倒计时。如果加载时间超过倒计时的时间,则这个组件不是首屏的时间计算之内。什么作为页面加载完成的依据?倒计时结束就不再获取组件的加载完成时间,得出来的页面最后加载的组件的时间就是首屏结束的时间
上代码:
import Vue from 'vue';
class whiteScreen {
constructor({ safeTime = 3000 } = {}) {
// 设置组件最大加载时间
this.safeTime = safeTime;
// 其他...
Vue.mixin({
mounted() {
// 如果不是正在加载中,则返回
if (!_this.isLoading()) return;
// 获取组件标签
const name = this.$options.name || this.$options._componentTag;
// 如果该组件已经加载过,则不用再记录
if (_this.isCompLoaded(name)) return;
this.$nextTick(() => {
if (name) {
_this.push({
name: name,
// 记录当前组件加载成功的时间
time: dayjs().format('YYYY-MM-DD HH:mm:ss'),
});
}
});
}
});
}
isLoading() {
return this.loading;
}
setLoading(value) {
this.loading = value;
if (!this.isLoading()) {
const data = [...this.times];
const startTime = this.startTime;
// TODO: 上传埋点
console.log({
data,
startTime,
});
}
}
createCountDown() {
window.clearTimeout(this.countTime);
this.countTime = window.setTimeout(() => {
this.setLoading(false);
}, this.safeTime);
}
isCompLoaded(name) {
if (!this.isLoadedComp[name]) {
this.isLoadedComp[name] = true;
return false;
}
return this.isLoadedComp[name];
}
push(item) {
// 重新创建定时器
this.createCountDown();
this.times.push(item);
}
}
这种方式是存在一定缺陷,虽然 safeTime 是可以传进来的,但是这个值不好设置,这里我们默认3秒,如果组件需要加载3秒,或者3秒内没有组件加载,我们视为首屏加载结束(注意:这里的倒计时不是从 window.onload 开始的,而且在第一个组件mounted完成的时候开始,所以算的组件加载完成到下一个组件加载完成是否超过3秒)
怎么兼容骨架屏完成的情况
这里我们可以取巧,骨架屏组件修改如下:
<div>
<span v-if="isOpen">我是骨架屏</span>
<span v-else>
// 这里可以写成空样式的组件
<skeleton-loaded></skeleton-loaded>
<slot></slot>
</span>
</div>
// skeleton-loaded
<span></span>
当骨架屏结束的时候,出现一个 skeleton-loaded 组件,那么这个组件会走mounted。被我们监听到,最后可以得到骨架屏的加载接触的情况
// 这个就是骨架屏组件加载结束的时间
const skeletonLoadedTime = this.times.find(item => item.name === 'skeleton-loaded').time
当然,这只是个例子,现实可以随你自己去发挥,确定什么是代表首屏结束的标志,好比我的真实业务情况,就是很简单,找到 element-ui 的 el-table 就可以了
// 这个就是 el-table 组件加载结束的时间
const elTableLoadedTime = this.times.find(item => item.name === 'el-table').time
结论
通过上面和结合perforemance,我们可以得出下面的时间:
- 所有组件加载的时间times
- 首屏的时间(刷新开始到最后一个时间结束):times[times.length - 1](最后一个组件的加载时间) - perforemance.timing.navigationStart(unload上一个文档的时间节点)
- 框架加载时间的时间:times[0](第一个组件加载的时间) - perforemance.timing.responseEnd(浏览器从服务器收到HTTP请求返回的最后一个字节的时间)
- 加载js资源所需要的时间:perforemance.timing.responseEnd(浏览器从服务器收到HTTP请求返回的最后一个字节的时间) - perforemance.timing.requestStart(浏览器向服务器发起HTTP请求(包含缓存,本地资源))
其实文中的思路其实特别简单,而且可以根据自己的需求来定制,兼容各种情况,有疑问可以在评论区提出。
谢谢观看,最后祝大家上线没bug,更多关于Vue首屏时间指标采集的资料请关注编程网其它相关文章!