Vue首屏时间指标采集最佳方式详解
作者:皮皮大人
前言
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而不是created,因为我们要记录白屏的时间 * 所以是用户看到界面的时刻,用mounted比created更加适合,具体看 vue 组件的生命周期 * 另外vue在组件和子组件加载机制。created和mounted执行时机也存在区别 */ 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而不是created,因为我们要记录白屏的时间 * 所以是用户看到界面的时刻,用mounted比created更加适合,具体看 vue 组件的生命周期 * 另外vue在组件和子组件加载机制。created和mounted执行时机也存在区别 */ 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首屏时间指标采集的资料请关注脚本之家其它相关文章!