Vue首屏时间指标采集最佳方式详解
作者:皮皮大人 发布时间:2024-06-05 09:17:24
前言
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首屏时间指标采集的资料请关注脚本之家其它相关文章!
来源:https://juejin.cn/post/7186651717960892474


猜你喜欢
- 网上关于这方面的文章有很多,重复的东西本文不再赘述,仅提供思路,并解释一些其他文章讲述模糊的地方。 1、使用meta标签,这也是普
- 首先预览一下 PyCharm 在实际应用中的界面:(更改了PyCharm的默认风格)安装首先去下载最新的pycharm 2.7.3,进行安装
- 笔者认为,在创建索引时要做到三个适当,即在适当的表上、适当的列上创建适当数量的索引。虽然这可以通过一句话来概括优化的索引的基本准则,但是要做
- 1 . 如何让自己的本地APACHE服务器支持.htaccess 如何让自己的本地APACHE服务器支持”.htaccess”呢?其实只要简
- 一、vscode插件介绍在我们演示插值表达式之前,我们先安装这一个VScode给我们提供的插件,它可以将我们书写好的网页通过服务端口的方式进
- 前言在前一篇文章中分享了编译器优化的变量捕获部分,本文分享编译器优化的另一个内容—函数内联。函数内联是指将将较小的函数内
- 介绍Prometheus 的基本原理是通过 HTTP 周期性抓取被监控组件的状态。任意组件只要提供对应的 HTTP 接口并且符合 Prome
- 注册模块default.asp 代码如下:<!DOCTYPE html PUBLIC "-//
- function commafy() { var num = document.getElementById("NumA"
- Django版本为:2.1.7Python的web框架,MTV思想MVCModel(模板文件,数据库操作) view(视图模板文
- 前言Pycharm学习过程中,每次在一个Pycharm窗口建立一个新的文件夹,都需要重新配置anaconda环境。由于本人是初学者,所以写一
- 由于我们分发的python应用可能运行在64位环境,也可能运行在32位环境,所以我们需要为同一套应用代码配置两套打包环境,怎么配置?步骤如下
- 大家都知道很多控件是没有clicked信号的,我在网上找了很多终于总结出2个方法来实现类似需求,比如给QLineEdit添加clicked信
- 网上有很多免费的ip地址,都是可以使用的,但是如果手动来获取太麻烦,这里通过Python自动抓取,可以批量获取。代码如下:# -*- cod
- 装饰器模式(Decorator Pattern)是什么装饰器模式是一种结构型模式,它允许你在运行时为一个对象动态地添加新的行为,而不影响其原
- 实现效果示例代码import timefrom selenium import webdriverfrom selenium.webdriv
- 本文实例讲述了Python lambda表达式用法。分享给大家供大家参考,具体如下:lambda表达式,通常是在需要一个函数,但是又不想费神
- 您是否知道 OpenCV 具有执行行人检测的内置方法?OpenCV 附带一个预训练的 HOG + 线性 SVM 模型,可用于在图像和视频流中
- 本文记录了PyCharm安装的图文教程,供大家参考,具体内容如下PyCharm的官网 1.在官网下载安装包2.选择Windows系
- 文件的io操作的缓冲行为分为全缓冲:同系统及磁盘块大小有关,n个字节后执行一次写入操作行缓冲:遇到换行符执行一次写操作无缓冲:立刻执行写操作