浅析Performance前端性能指标收集上报

2,433 阅读4分钟

Performance

Performance API是W3C性能工作小组引入的一个新 API,下面就来说说performance中的timing属性,下图是timing属性中的字段:

如需了解performance其它属性和方法,请参考 MDN里的详细说明

从上图可以看到各种字段可以看到,我们网页从发起请求开始到整个网页资源加载完成的时间都有,下面我们再来看张图,就能清晰了解到这些字段属于网页资源加载的哪个阶段。

通过上过,我们能清晰的知道网页加载的每个阶段的开始和结束时间,那么通过对应阶段的结束时间减去开始时间,就能得到我们网页在每个阶段的请求耗时了,请看下面代码:

	
function perfData(timing){
	return {
      // DNS查找时间
      dns: timing.domainLookupEnd - timing.domainLookupStart,
      // TCP建连时间
      connect: timing.connectEnd - timing.connectStart, 
      // 首字节时间
      ttfb: timing.responseStart - timing.navigationStart, 
      // 白屏时间
      whiteScreen: timing.domLoading - timing.navigationStart,
      // 上一个页面到当前页面的时间
      prevPage: timing.fetchStart - timing.navigationStart, 
      // 重定向时间
      redirect: timing.redirectEnd - timing.redirectStart, 
      // 请求从发送到接收的时间
      send: timing.responseStart - timing.requestStart, 
      // 网络总耗时
	  network: timing.connectEnd, timing.navigationStart, 
      // dom解析时间
      dom: timing.domComplete - timing.domLoading, 
      // DOM准备时间
      domReady: timing.domLoading - timing.domInteractive, 
      // loadEvent时间
      loadEvent: timing.loadEventEnd - timing.loadEventStart, 
      // 可操作时间
      interactive: timing.domInteractive - timing.navigationStart, 
      // 页面完全加载的时间,总时长
      total: timing.loadEventEnd - timing.navigationStart
    }
}
// 调用
perfData(performance.timing)

从上面的代码我们就能得到关于网页加载时一些关键数据,但上面代码现在并不能正确,看下图: 运行得到结果有一些值居然负数,出现这种情况主要是因为整个网页没有加载完成的情况下就调用了这方法,所以下面我们进行改进,让时间正确显示。

window.addEventListener('load', () => {
    console.log(perfData(performance.timing))
}, false)

那么在load里面调用,时间总该正常了吧?但此时数据loadEventtotal字段还是为负的,是因为执行load事还是处于onload阶段,performance.timing.loadEventEnd 为0,所以导致loadEventtotal字段还是为负的,既然知道了原因,那么就比较容易解决了,看下代码:

function load(){
  let timer = null;
  let runCheck = ()=>{
  	  // 既然调用load的时候loadEventEnd会为0
      // 那么就判断,如果值不为0,就调用perfData方法收集性能指标
      if (performance.timing.loadEventEnd) {
          clearTimeout(timer)
		  // 打印
          console.log(perfData(performance.timing))
      } else {
          // 否则就定时调用runCheck方法,直到loadEventEnd不为0。
          timer = setTimeout(runCheck, 100);
      }
  }
  window.addEventListener('load', runCheck , false)
}

// 调用
load()

通过上面代码,我们解决部分字段为负的情况,但上述考虑的是网页加载完成的情况,还有一种情况是页面还没加载完成就被用户关闭了或因资源太大导致页面一直不能加载完成的情况,因此我们还应该进行dom解析完成后的就进行一次数据收集。

function domReady(){
  let timer = null;
  let runCheck = ()=>{
      if (performance.timing.domComplete) {
          clearTimeout(timer)
		  // 打印
          console.log(perfData(performance.timing))
      } else {
          timer = setTimeout(runCheck, 100);
      }
  }
  window.addEventListener('DOMContentLoaded', runCheck , false)
}
// 调用
domReady()

domReady总体来说和load相似,判断由loadEventEnd 字段变为 domCompletedomComplete表示dom解析结束时间,然后load事件变为DOMContentLoaded事件,现在我们性能指标都能正常收集,可以提交给后端进行存储,这里我们以new Image()图片方式进行数据提交,完整代码如下:

function performanceCollect(callback) {
    const collect = {
        initData: (timing) => ({
            // DNS查找时间
            dns: timing.domainLookupEnd - timing.domainLookupStart,
            // TCP建连时间
            connect: timing.connectEnd - timing.connectStart,
            // 首字节时间
            ttfb: timing.responseStart - timing.navigationStart,
            // 白屏时间
            whiteScreen: timing.domLoading - timing.navigationStart,
            // 上一个页面到当前页面的时间
            prevPage: timing.fetchStart - timing.navigationStart,
            // 重定向时间
            redirect: timing.redirectEnd - timing.redirectStart,
            // 请求从发送到接收的时间
            send: timing.responseStart - timing.requestStart,
            // dom解析时间
            dom: timing.domComplete - timing.domLoading,
            // DOM准备时间
            domReady: timing.domContentLoadedEventStart - timing.navigationStart,
            // load 时间
            loadEvent: timing.loadEventEnd - timing.loadEventStart,
            // 可操作时间
            interactive: timing.domInteractive - timing.navigationStart,
            // 页面完全加载的时间,总时长
            total: timing.loadEventEnd - timing.navigationStart
        }),
        domReady: (cb) => {
            let timer = null;
            let runCheck = () => {
                if (performance.timing.domComplete) {

                    clearTimeout(timer)
                    cb()
                } else {
                    timer = setTimeout(runCheck, 100);
                }
            }
            window.addEventListener('DOMContentLoaded', runCheck, false)
        },
        load: (cb) => {
            let timer = null;
            let runCheck = () => {
                // 既然调用load的时候loadEventEnd会为0
                // 那么就判断,如果值不为0,就调用perfData方法收集性能指标
                if (performance.timing.loadEventEnd) {
                    clearTimeout(timer)
                    cb()
                } else {
                    // 否则就定时调用runCheck方法,直到loadEventEnd不为0。
                    timer = setTimeout(runCheck, 100);
                }
            }
            window.addEventListener('load', runCheck, false)
        }
    }
    collect.load(() => {
        const initData = collect.initData(performance.timing)
        // 添加一个类型,便于识别是load时提交的,还是domReady时提交的
        initData.type = 'load';
        callback(initData)
    })
    collect.domReady(() => {
        const initData = collect.initData(performance.timing)
        // 添加一个类型,便于识别是load时提交的,还是domReady时提交的
        initData.type = 'domReady';
        callback(initData)
    })
}

// 序列化对象,将对象转成换 'a=1&b=2'格式
function formatObj(data){
	let arr = [];
    for (let key in data) {
        arr.push(`${key}=${data[key]}`)
    }
    const perData = arr.join('&');
    return perData
}

performanceCollect((data) => {
    // 上传
    new Image().src = `./1.gif?${formatObj(data)}`
})