以用户为中心的性能指标获取

1,513 阅读3分钟

以用户为中心的性能指标

用户体验描述
是否发生?导航是否成功启动?服务器是否有响应?
是否有用?是否已渲染可以与用户互动的足够内容?
是否可用?用户可以与页面交互,还是页面仍在忙于加载?
是否令人愉悦?交互是否顺畅而自然,没有滞后和卡顿?

是否发生

first paint / first contentful paint

first paint: 首个元素绘制的时间。
first contentful paint: 首个内容绘制时间,具体指图片或者文本的首个像素渲染。
从定义上这两个指标定义上有所不同,但目前获取的数据却是一致的,我们可以把这两个指标看成我们平时所说的白屏时间。

计算方式

计算方式通过性能对象performance来实现,后面的很多个指标都是利用该对象来获取的,它保存了页面中每一个http请求统计信息。点击这里了解更多关于performance对象。 这个对象在大部分浏览器都支持了,除了IE。

var paintEntries = performance.getEntriesByType('paint');
for (var i = 0; i < paintEntries.length; i++) {
  var entry = paintEntries[i]
  console.log('entry name:', entry.name)
  console.log('spent time', entry.startTime + entry.duration)
}

是否有用

first meaningful paint

第一个有意义的元素渲染时间,有意义指的就是首屏重要元素,所以可以理解为首屏重要元素的渲染,当这个元素渲染出来后,则说明了我们的页面可用了。

计算方式

由于每个页面重要元素不甚相同,所以这个需要用户打点来收集,分别在最开始的位置和重要元素渲染后分别打点。

// 在head的最前面打开始的点
performance.mark('start fmp')
// 在重要元素渲染后打结束的点
performance.mark('end fmp')
// 计算first meaningful paint时间
var entry = performance.measure('fmp', 'start fmp', 'end fmp')
console.log('spent time', entry.startTime + entry.duration)

关于开始打点的位置,我们可以在head的最前端添加script代码,作为开始的点。
关于重要元素渲染后的打点,如果我们用react/vue,我们可以把这个重要元素提取为一个组件,然后在componentDidMount中打点。如果是vue,则在mounted方法打点。如果是普通html,则可以在重要到元素下面添加script,然后script里面打点。
如果图片是重要元素,则可以计算首图时间来作为fmp,后面会讲到。

首图加载时间

首图时间是页面中首张图片的加载时间,计算首屏时间的时候,如果首屏中有图片,则首屏时间就是首图时间。

计算方式

计算首图时间,我们同样可以利用performance来获取图片加载详情。我们需要标记哪些图片是首图,这里我们给首图元素(不一定是img元素)加个perf-img="true"来标示首图元素,可以多个。

// html
<img src={logo} width={224} perf-img="true"/>
// js

// 获取首图元素的图片地址,首图元素可能是img/元素的background
function getImgSrc(dom) {
    var imgSrc;
    if (dom.nodeName.toUpperCase() == 'IMG') {
      imgSrc = dom.src;
    } else {
      var computedStyle = window.getComputedStyle(dom);
      var bgImg = computedStyle.getPropertyValue('background-image') ||         computedStyle.getPropertyValue('background');
      var matches = bgImg.match(/url\(.*?\)/g);
      if (matches && matches.length) {
        var urlStr = matches[matches.length - 1]; // use the last one
        var innerUrl = urlStr.replace(/^url\([\'\"]?/, '').replace(/[\'\"]?\)$/, '');
        if (((/^http/.test(innerUrl) || /^\/\//.test(innerUrl)))) {
          imgSrc = innerUrl;
        }
      }
    }
    return imgSrc;
}
var entries = performance.getEntriesByType('resource');
for (var i = 0; i < paintEntries.length; i++) {
    var entry = entries[i];
    if (entry.initiatorType === 'img') {
        var $mainImgs = document.querySelectorAll('[perf-img]');
        var len = $mainImgs.length;
        for (var i = 0; i < len; i++) {
            var $mainImg = $mainImgs[i];
            // 如果加载的是首图图片
            if (entry.name === getImgSrc($mainImg)) {
              console.log('spent time', entry.startTime + entry.duration)
            }
        }
    }
}

是否令人愉悦

long task

js是单线程的,所有的任务都需要放在主线程的队列中执行,浏览器的ui任务和js任务都需要放在队列中等待主线程空闲后执行,如果js任务耗时较长,则会导致ui无法渲染,用户无法和页面交互,用户就会感知到页面滞后或者卡顿。获取long task需要利用PerformanceObserver对象,这个对象可以监控long task。

计算方式

  var longTaskObserver = new PerformanceObserver((list) => {
    var entries = list.getEntries();
    for (var i = 0, len = entries.length; i < len; i++) {
      var entry = entries[i];
      var time = entry.startTime + entry.duration;
      console.log('long task spent', time);
    }
  });

  longTaskObserver.observe({ entryTypes: ['longtask'] });

是否可用

time to interactive

简称tti,可交互时间,表示用户是否可以通过点击、输入等和页面交互。

计算方式

谷歌提供了ttiPolyfill的sdk来计算,实际上是计算long tas,它把onload为起点,以5秒作为一个时间窗口,找到一个没有long task并且没有两个以上未完全的请求的时间窗口来上报tti。

我们也可以像计算fmp一样的方法来计算tti,在可交互的元素渲染后打点,虽然需要手动打点,但比较可靠。

其他

首api(重要api)加载时间 / 首js(重要js)加载时间 / 首css(重要css)加载时间

这几个指标同样重要: 如果重要api未加载下来,页面可能无法显示,则页面“不可用”-是否可用。
如果重要js未加载下来,页面就是白屏,则页面“没发生”-是否发生。
如果重要css未加载下来,页面没有样式,则页面“不可用”为否-是否可用。

计算方式

这几个指标的计算方式同首图的计算方式一样,通过performance获取resource类型的entry即可。

var __performance = {
    firstApi: 0,
    firstCss: 0,
    firstJs: 0,
    firstImg: 0
}

var entries = performance.getEntries('resource');
for (var i = 0, len = entries.length; i < len; i++) {
  var entry = entries[i];
  const time = entry.startTime + entry.duration;
  switch (entry.initiatorType) {
    case 'xmlhttprequest':
      if ("/api/v1/users/profile" === entry.name || "/api/v1/users/status")) {
        // 直接覆盖上个的值,至于为什么,下面会提到
        __performance.firstApi = time;
      }
      break;
    case 'img':
      var $mainImgs = document.querySelectorAll('[perf-img]');
      var len = $mainImgs.length;
      for (var i = 0; i < len; i++) {
        var $mainImg = $mainImgs[i];
        if (entry.name === getImgSrc($mainImg)) {
            __performance.firstImg = time;
        }
      }
      break;
    case 'link':
      if (/app.*\.css/.test(entry.name)) {
        __performance.firstCss = time;
      }
      break;
    case 'script':
      if (/app.*\.js/.test(entry.name)) {
        __performance.firsJs = time;
      }
      break;
  }
}

同个指标多个值问题

对于首图、首api、首js、首css,都可能包含多个,我们以首api为例。
如果first api有多个,那么以哪个为准还是两个的值叠加呢?这分两种情况讨论:
1、多个并行请求,我们以耗时最长的请求为准。 2、多个请求串行,则应该多个请求的时间叠加合。
对于第一种情况,我们获取数据的方法很简单,以请求耗时最长的为准:

对于第二种情况,我们需要叠加两个请求的startTime+duraton吗?实际上不需要,PerformanceObserver帮我们做了这个工作,第二个请求的startTime=第一个请求耗时,所以我们获取指标的时候还是这样:

var time = entry.startTime + entry.duration

最后以time最大的值为准。

何时获取&上报数据

最理想的情况就是当页面稳定的时候获取然后上报数据,这样可以获取到全面的数据。但是页面稳定的时候是个无解的问题,我们只能在页面onload后取个合适的时间,这个时间建议5s,如果5s后页面还没稳定,用户就会觉得卡顿,这和2-5-8原则相关。 用户感知长度表:

用户感知响应时间
流畅< 2s
可用2s ~ 5s
卡顿5s ~ 8s
阻塞> 8s
function report() {
    setTimeout(function() {
        // 利用上面的代码获取到各个指标后这里获取
        console.log(window.__performance)
    }, 5000)
}
if (document.readyState == 'complete') {
    report();
} else {
    window.addEventListener('load', () => {
      report();
    });
}

对于如何判断页面5s后某些指标仍然未获取得到,则判定为页面有阻塞:

  1. fp/fcp 5s后为0(一直白屏)
  2. 首配置了首js,5s后仍然为0(说明5s后js未加载下来)
  3. 如果配置了首api,5s后仍然为0(说明5s后接口还未拉取回来)
  4. 如果手动打点fmp,5s后仍然为0或者fmp耗时超过5s(重要元素渲染超过5s)
  5. 如果手动打点了tti,tti耗时超过5s(页面到可响应时间超过5s)
  6. long task超过5s(js阻塞超过5s)

如果判定页面阻塞,则5s后我们获取到的数据就是超时。

最后附上源码:
源码

参考链接