应用:前端性能监控performance

9,652 阅读14分钟

  项目上线后,通常都是都会做埋点,做数据监控、异常监控以及性能监控,本文主要聊聊前端性能监控。重点讲performance怎么做性能监控。

  前端性能监控通常检测某些方面的加载时间,通过得到的加载时间的长短来判断项目某方面的性能怎样,具体是哪些方面呢???我们先来看下页面加载是一个怎样的过程:

页面加载

  页面加载的方式有两种,一种是加载完资源文件后通过javascript动态获取接口数据,然后数据返回渲染内容,这就是前后端分离的项目页面加载方式。另一种就是服务器渲染,同构直出前端页面,这种方式相对前一种方式页面加载的速度要快很多,性能方面相对也就要好些。但是目前大部分的项目都是前后端分离,本文重点描述这种方式。

  1. 输入网址,回车
  2. 缓存解析:如果浏览器本地缓存有资源从缓存取资源
  3. 域名解析:DNS解析,将域名解析成IP,如果缓存中存在,直接丛缓存中取IP,不用做域名解析
  4. 发送请求:向服务器发送请求
  5. TCP连接、三次握手:建立浏览器端和服务器端连接
  6. 服务器接到请求:服务器响应请求
  7. 数据传输
  8. 浏览器端拿到数据,解析html文件,构建DOM树,CSSOM树,js文件的加载可能会阻塞页面的渲染
  9. 初始的html被完全加载和解析后会触发DOMContentLoaded事件
  10. CSSOM树和DOM树构建完成后会开始生成Render树,绘制
  11. 在生成Render树的过程中,浏览器就开始调用GPU绘制,合成图层,将内容显示在屏幕上
  12. 没有文件传输,四次挥手,TCP连接断开

  上面就是页面加载的基本过程,没有展开讲,如果要展开讲的话,十篇文章也讲不完,在本文中只需要清楚大概的整个过程,后期安排系统的整理上述整个过程的内容,做文字输出。

  页面加载整个过程中主要分为:白屏、重定向、DNS查询、TCP连接、HTTP请求、DOM解析、DOMreadyonload等,这些也就是我们前端性能监控包括的主要方面。我们需要监控这几个方面的数据,做数据分析,进一步做前端性能优化。例如如何加快首屏加载时间、减少http请求时间等等。

  下面讲讲本文主角,performance,不是Chrome开发者工具的Performance面板,当然Chrome开发者工具也能做性能监控。

performance

  performance是前端性能监控的API,可以获取到当前页面中也与性能相关的信息。

  我们来看看天猫商城首页通过window.performance这个API,获取到的一些信息:

  从上面的信息可以看到window.performance是一个对象,包含了四个属性:memorynavigationtimeOrigintiming,以及一个事件处理程序onresourcetimingbufferfull,我们再来看看这几个分别代表什么?

performance.memory

  在Chrome中添加的一个非标准扩展,memory这个属性提供了一个可以获取到基本内存使用情况的对象MemoryInfo

performance.memory = {
  jsHeapSizeLimit, // 内存大小限制,单位是字节B
  totalJSHeapSize, // 可使用的内存大小,单位是字节B
  usedJSHeapSize   // JS对象占用的内存大小,单位是字节B
}

  如果usedJSHeapSize的值大于totalJSHeapSize,会出现内存泄露的问题,所以不能大于totalJSHeapSize的值。

performance.navigation

  返回PerformanceNavigation对象,提供了在指定的时间段发生的操作相关信息,包括页面是加载还是刷新、发生了多少重定向等。

performance.navigation = {
  redirectCount: xxx,
  type: xxx
}

  PerformanceNavigation对象有两个属性,redirectCounttype

performance.navigation.redirectCount

  只读属性,表示达到这个页面之前经过多少次重定向跳转,但是这个接口有同源策略的限制,仅能检测到同源的重定向。

performance.navigation.type

  只读属性,有四个返回值:0,1,2,225:

  • 0:表示当前页面是通过点击链接,书签和表单提交,或者脚本操作,或者在url中直接输入地址,相当于常数performance.navigation.TYPE_NAVIGATE
  • 1:表示点击刷新页面按钮或者通过Location.reload()方法显示的页面,相当于常数performance.navigation.TYPE_RELOAD
  • 2:表示页面通过历史记录和前进后退访问的,相当于常数performance.navigation.TYPE_BACK_FORWARD
  • 225:表示任何其他加载方式,相当于常数performance.navigation.TYPE_RESERVED

performance.timeOrigin

  返回性能测量开始的时间的高精度时间戳。

  上面的时间就表示开始性能测试的时间。

performance.onresourcetimingbufferfull

  一个回调的EventTarget,当触发resourcetimingbufferfull事件的时候会被调用。

performance.timing

  返回PerformanceTiming对象,包含了各种与浏览器性能相关的数据,提供了浏览器处理页面的各个阶段的耗时,其整体结构可以参考下图:

  PerformanceTiming对象中的属性都是只读属性,值都是精确到Unix毫秒的时间戳:

navigationStart

  返回当前浏览器窗口的前一个页面的关闭,发生unload事件时的时间戳。如果没有前一个页面,则等于fetchStart属性。

unloadEventStart

  返回如果前一个页面与当前页面同域,则返回前一个页面unload事件发生时的时间戳。如果没有没有前一个页面,或者之前的页面跳转不是在同一个域名内,则返回值为0

unloadEventStart

  和unloadEventStart相对应,返回前一个页面unload事件绑定的回调函数执行完毕的时间戳。如果没有没有前一个页面,或者之前的页面跳转不是在同一个域名内,则返回值为0

redirectStart

  返回第一个http重定向发生时的时间戳。有跳转并且是同域名内的重定向,否则返回值为0

redirectEnd

  返回最后一个http重定向完成时的时间戳。有跳转并且是同域名内的重定向,否则返回值为0

fetchStart

  返回浏览器准备好使用http请求抓取文档的时间戳,这发生在检查本地缓存之前

domainLookupStart

  返回DNS域名查询开始的时间戳,如果使用了本地缓存(也就是没有做DNS查询,直接从缓存中取到IP)或者使用了持久连接,则与fetchStart值相等

domainLookupEnd

  返回DNS域名查询完成的时间戳,如果使用了本地缓存(也就是没有做DNS查询,直接从缓存中取到IP)或者使用了持久连接,则与fetchStart值相等

connectStart

  返回httpTCP)开始建立连接的时间戳,如果是持久连接,则与fetchStart值相等。如果在传输层发生了错误并且重新建立连接,则这里显示的是新建立的连接开始的时间戳

connectEnd

  返回httpTCP)完成建立连接的时间戳,完成了四次握手,如果是持久连接,则与fetchStart值相等。如果在传输层发生了错误并且重新建立连接,则这里显示的是新建立的连接完成的时间戳。连接建立指的是所有握手和认证过程全部结束

secureConnectionStart

  返回https连接开始的时间戳,如果不是安全连接,否则返回值为0

requestStart

  返回http请求读取真实文档开始的时间戳(完成建立连接),包括从本地读取缓存。如果连接错误重连时,这里显示的也是新建立连接的时间戳

responseStart

  返回http开始接收响应的时间戳(获取到第一个字节),包括从本地读取缓存

responseEnd

  返回http响应全部接收完成的时间戳(获取到最后一个字节),包括从本地读取缓存

domLoading

  返回开始解析渲染DOM树的时间戳,此时Document.readyState变为loading,并将抛出readystatechange相关事件

domInteractive

  返回完成解析DOM树的时间戳,Document.readyState变为interactive,并将抛出readystatechange相关事件。这里只是DOM树解析完成,这时候并没有开始加载网页内的资源

domContentLoadedEventStart

  返回DOM解析完成后,网页内资源加载开始的时间戳。即所有需要被执行的脚本开始被解析了。在DOMContentLoaded事件抛出前发生

domContentLoadedEventEnd

  返回DOM解析完成后,网页内资源加载完成的时间戳。例如JS脚本加载执行完成,不论执行顺序。DOMContentLoaded事件也已经完成

domComplete

  返回DOM解析完成,且资源也准备就绪的时间戳。Document.readyState变为complete,并将抛出readystatechange相关事件

loadEventStart

  返回load事件发送给文档,load回调函数开始执行的时间戳。如果没有绑定load事件,返回值为0

loadEventEnd

  返回load事件的回调函数执行完毕的时间戳。如果没有绑定load事件,返回值为0

  上面已经解释了相关属性的含义,通过上面的数据能做很多帮助我们做性能监控的事情:

  • 页面加载完成时间:代表了用户等待页面可用的时间
let performance = window.performance;
let t = performance.timing;
let time = t.loadEventEnd - t.navigationStart;
  • 解析DOM树结构的时间:判断DOM树嵌套情况
let performance = window.performance;
let t = performance.timing;
let time = t.domComplete - t.responseEnd;
  • 重定向的时间
let performance = window.performance;
let t = performance.timing;
let time = t.redirectEnd - t.redirectStart;
  • DNS查询时间:可做预加载,缓存,减少查询时间
let performance = window.performance;
let t = performance.timing;
let time = t.domainLookupEnd - t.domainLookupStart;
  • 白屏时间:读取页面第一个字节的时间
let performance = window.performance;
let t = performance.timing;
let time = t.responseStart - t.navigationStart;
  • 内容加载完成的时间
let performance = window.performance;
let t = performance.timing;
let time = t.responseEnd - t.requestStart;
  • 执行onload回调函数的时间
let performance = window.performance;
let t = performance.timing;
let time = t.loadEventEnd - t.loadEventStart;
  • DNS缓存时间
let performance = window.performance;
let t = performance.timing;
let time = t.domainLookupStart - t.fetchStart;
  • 卸载页面的时间
let performance = window.performance;
let t = performance.timing;
let time = t.unloadEventEnd - t.unloadEventStart;
  • TCP建立连接完成握手的时间
let performance = window.performance;
let t = performance.timing;
let time = t.connectEnd - t.connectStart;

  我们可以计算出页面加载的过程中各阶段的耗时,根据时长判断某阶段的性能怎么样,再做进一步的优化处理。这是性能优化很重要的一步,我们需要定位到具体的哪个阶段耗时过长,对症下药。

  performance也提供了一些方法,我们来看看一些常用的方法:

自定义统计方法

performance.mark()

  创建一个DOMHighResTimeStamp保存在资源缓存数据中,可通过performance.getEntries()等相关接口获取。简单的理解就是可以做标记,也就是“打点”

performance.mark(name);

performance.clearMarks()

  用于清除标记,如果不加参数,就表示清除所有标记。

performance.clearMarks(name); // 清除指定标记
performance.clearMarks(); // 清除所有标记

performance.measure()

  计算两个mark之间的时长,创建一个DOMHighResTimeStamp保存在资源缓存数据中,可通过performance.getEntries()等相关接口获取。

performance.measure(name, startMark, endMark);

performance.clearMeasures

  移除缓存中所有entryTypemeasure的资源数据。

performance.clearMeasures(name); // 清除指定记录间隔数据
performance.clearMeasures(); // 清除所有记录间隔数据

  上面的四个API,可以自定义统计一些数据,例如统计某函数的执行时间。

  在Vue中也有用到,为了追踪组件的性能,在Vue2.X中全局配置API有这么个方法:

Vue.config.performance = false;

  设置为true以在浏览器开发工具的性能/时间线面板中启用对组件初始化、编译、渲染和打补丁的性能追踪。只适用于开发模式和支持performance.mark API的浏览器上。我们入口文件中开启,开启后可以使用Vue Performance Devtool 这个chrome插件来查看各组件加载情况:

if (process.env.NODE_ENV !== 'production') {
  Vue.config.performance = true;
}

  在Vue源码中,也是通过performance.markperformance.measure来实现的,我们看下具体的源码实现:

// vue/src/core/util/perf.js
import { inBrowser } from './env'

export let mark
export let measure

if (process.env.NODE_ENV !== 'production') { // 环境判断 开发环境执行下面程序
  const perf = inBrowser && window.performance // 浏览器环境
  /* istanbul ignore if */
  if (
    perf &&
    perf.mark &&
    perf.measure &&
    perf.clearMarks &&
    perf.clearMeasures
  ) {
    mark = tag => perf.mark(tag) // 给定tag打点,做标记
    measure = (name, startTag, endTag) => {
      perf.measure(name, startTag, endTag) // 计算两个mark之间的时长
      perf.clearMarks(startTag) // 清除startTag标记
      perf.clearMarks(endTag) // 清除endTag标记
      // perf.clearMeasures(name) // 清除指定记录间隔数据
    }
  }
}

  从上面的代码可以看出,尤大大通过markmeasure两个函数对performance.mark()performance.measure()进行了封装。我们再来看看在源码中怎么应用的:

// vue/src/core/instance/init.js
import { mark, measure } from '../util/perf'
export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    vm._uid = uid++
    let startTag, endTag
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag) // 开始标记
    }
    // .... 中间代码省略
    // 一系列初始化函数
    // initLifecycle(vm)
    // ....
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag) // 结束标记
      measure(`vue ${vm._name} init`, startTag, endTag) // 计算两个mark之间的时长
    }
  }
}

  上面是Vue初始化实现的相关程序,上面的打点标记就是为了追踪组件初始化的性能情况,就是在初始化的代码的开头和结尾分别使用mark函数打上两个标记,然后通过measure函数对这两个标记点进行性能计算。在编译、渲染和打补丁也做了性能追踪,感兴趣的同学可以看看。

  上面是Vue中对performance的应用,起初看vue源码的时候看到这块不是很明白,不影响整体的阅读,没有太关系,最近工作上有用到performance,想到Vue好像也用到了,就回头看看Vue中的应用,现在是整明白了。我们再来看看一个比较简单实例,更好的理解下:

performance.mark('markStart'); // 标记一个开始点
for(let i = 0; i < 10; i++) {
  console.log(i);
}
performance.mark('markEnd'); // 标记一个结束点
performance.measure('measureVal', 'markStart', 'markEnd');
let measures = performance.getEntriesByName('measureVal');
let measure = measures[0]
console.log('milliseconds: ', measure.duartion); // 0.8999999990919605
// 清除标记
performance.clearMarks();
performance.clearMeasures();

  上面就可以通过提供的API计算出for循环的执行时间。这几个API在性能监控应用比较频繁。

性能资源获取方法

performance.getEntries

  获取所有资源请求的时间数据,这个函数返回一个按startTime排序的对象数组:

  从上面可以看出,返回都是资源页面加载的相关数据,很多属性与performance.timing一样,在这就不再解释了。在这里梳理其他几个重要的属性:

  • name: 资源名称,是资源的绝对路径或调用mark方法自定义的名称(例如entryTyperesource时,name表示资源的路径)。
  • duration,一个DOMHighResTimeStamp对象,获取该资源消耗时长。
  • startTime,一个DOMHighResTimeStamp对象,开始获取该资源的时间。
  • entryType
该类型对象 描述
mark PerformanceMark 通过mark()方法添加到数组中的对象
measure PerformanceMeasure 通过measure()方法添加到数组中的对象
paint PerformancePaintTiming 值为first-paint首次绘制、first-contentful-paint首次内容绘制
resource PerformanceResourceTiming 所有资源加载时间,用处最多
navigation PerformanceNavigationTiming 现除chromeOpera外均不支持,导航相关信息
frame PerformanceFrameTiming 现浏览器均未支持
  • initiatorType,初始化该资源的资源类型:
发起对象 描述
a Element link/script/img/iframe 通过标签形式加载的资源,值是该节点名的小写形式
a CSS resourc css 通过css样式加载的资源,比如backgroundurl方式加载资源
a XMLHttpRequest object xmlhttprequest/fetch 通过xhr加载的资源
a PerformanceNavigationTiming object navigation 当对象是PerformanceNavigationTiming时返回

performance.getEntriesByName

  根据参数nametype获取一组当前页面已经加载的资源数据。name的取值对应到资源数据中的name字段,type取值对应到资源数据中的entryType字段。

let entries = window.performance.getEntriesByName(name, type);

performance.getEntriesByType

  根据参数type获取一组当前页面已经加载的资源数据。type取值对应到资源数据中的entryType字段

let entries = window.performance.getEntriesByType(type);

  getEntriesByNamegetEntriesByType可以通过指定参数获取某类型的资源数据,这样我们可以资源数据分类统计,得出各类数据的情况。

  上面就是performance的主要内容了,可以通过这个API做很多关于性能监控的事情,需要根据自己具体的场景制定合适的方案。熟悉Chrome开发者工具的同学,也是可以通过performance面板来做性能监控,那为啥还提供API来做呢,其实更多的希望通过打点统计数据量,可以通过可视化的方式对数据分析,更直观,进而做下一步操作。

结语

  文章如有不正确的地方欢迎各位大佬指正,也希望有幸看到文章的同学也有收获,一起成长!

——本文首发于个人公众号———

最后,欢迎大家关注我的公众号,一起学习交流。