Vue首屏时间指标采集最佳方式详解

软件发布|下载排行|最新软件

当前位置:首页IT学院IT技术

Vue首屏时间指标采集最佳方式详解

皮皮大人   2023-02-02 我要评论

前言

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 =&gt; item.name === 'skeleton-loaded').time 

当然,这只是个例子,现实可以随你自己去发挥,确定什么是代表首屏结束的标志,好比我的真实业务情况,就是很简单,找到 element-ui 的 el-table 就可以了

// 这个就是 el-table 组件加载结束的时间
const elTableLoadedTime = this.times.find(item =&gt; 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首屏时间指标采集的资料请关注其它相关文章!

Copyright 2022 版权所有 软件发布 访问手机版

声明:所有软件和文章来自软件开发商或者作者 如有异议 请与本站联系 联系我们