使用 Performance API 获取页面性能

12,348 阅读11分钟

一 概述

1. 什么是 Performance

首先 Performance 是一个标准,用于解决开发者在浏览器中获取性能数据的问题。其次, Performance 是一个浏览器全局对象,提供了一组 API 用于编程式地获取程序在某些节点的性能数据。它包含一组高精度时间定义,以及配套的相关方法。

2. 新的 Performance 标准

新的 Performance 标准包含了许多 API,它们各自针对性能监控的某个方面。但是目前 Performance 部分标准处于实验阶段,并未稳定发布,且已发布的标准,各浏览器支持度差异较大。

API描述状态兼容性
Navigation Timing导航计时。用于记录并检测用户的设备,网络等环境,以及页面初始资源加载和解析耗时。稳定>= IE 9
High Resolution Timing精确计时。用于精确到微秒级别的计时,实际使用时精度受浏览器实现限制,通常精确到毫秒稳定>= IE 10
Resource Timing资源计时。对单个网络资源的计时,包含页面从打开到获取时的所有网络资源。稳定>= IE 10, Safari 不支持
Page Visiblity页面可见性。获取页面是否可见(例如用户切换了标签页,当前页面隐藏,此时为不可见)稳定>= IE 10
Battery Status电池状态。获取当前设备是否充电,电量等级等。对移动设备来说非常实用。稳定IE, Safari不支持, Android WebView >=40
User Timing用户计时。手动对某段代码或函数计时。稳定>= IE 10, Firfox 40, safari 11
Beacon灯塔。用于将分析结果和诊断数据采用非阻塞异步的方式发送到服务器,Beacon 方法保证在页面关闭前执行。实验IE, Safari, IOS, Android 不支持
Frame Timing帧计时。获取与帧相关的性能数据,例如每秒帧数实验不支持

3. 旧的 Performance 标准

PerformanceTiming 接口是为了向下兼容而存在的接口。它包含了全部的 Navigation Timing 以及部分 Resource Timing 的数据。PerformanceTiming 对象的兼容性是最好的,通过这个接口可以获取到页面从点击链接到页面渲染完毕的各个关键时间节点。

4. 如何获取 Performance

通过 window.performance 来获取 Performance 对象。

window.performance
// Performance {
//   timeOrigin: 1611749316627.347, 
//   onresourcetimingbufferfull: null, 
//   eventCounts: EventCounts, 
//   timing: PerformanceTiming, 
//   navigation: PerformanceNavigation, 
//   ...
// }

PerformanceEntry 接口可以分类获取 performance 运行时的实例数据,例如手动创建的 mark 标记产生的性能数据,或者是在异步资源加载时(css,script,ajax,等)被动创建的资源标记

二 PerformanceTiming

1. 概述

PerformanceTiming 接口提供了在页面加载和使用时的各种性能计时信息。通过 window.performance.timing 获得一个实例对象。

2. 属性

PerformanceTiming 接口的所有属性都是只读的,且没有任何继承属性,由于所有属性均是某个时间点,因此值类型都是 无符号 long long 类型(double)

(1) PerformanceTiming.navigationStart

PerformanceTiming.navigationStart 表示同一个浏览器上下文中,上一个文档卸载结束的 UNIX 时间戳。如果没有上一个文档,这个值与 PerformanceTiming.fetchStart 相同。

(2) PerformanceTiming.unloadEventStart

PerformanceTiming.unloadEventStart 表示 unload 事件抛出时的 UNIX 时间戳。如果没有上一个文档,或者重定向中的一个与当前文档不同源,该值为 0

(3) PerformanceTiming.unloadEventEnd

PerformanceTiming.unloadEventEnd 表示 unload 事件处理完成时的 UNIX 时间戳。如果没有上一个文档,或者重定向中的一个与当前文档不同源,该值为 0

(4) PerformanceTiming.redirectStart

PerformanceTiming.redirectStart 表示第一个 HTTP 重定向开始时的 UNIX 时间戳。如果没有重定向,或者重定向中的一个不同源,该值为 0

(5) PerformanceTiming.redirectEnd

PerformanceTiming.redirectEnd 表示最后一个 HTTP 重定向完成时(即最后一个 HTTP 响应的最后一个比特被接收到的时间)的 UNIT 时间戳。如果额米有重定向,或者重定向中的一个不同源,该值为 0

(6) PerformanceTiming.fetchStart

PerformanceTiming.fetchStart 表示浏览器准备好用 HTTP 请求来获取文档的 UNIX 时间戳。这个时间早于检查应用缓存。

(7) PerformanceTiming.domainLookupStart

PerformanceTiming.domainLookupStart 表示域名查询开始的 UNIX 时间戳。如果使用了持续连接,或者这个信息被存储到了缓存或本地资源,那么该值与 PerformanceTiming.fetchStart 相同。

(8) PerformanceTiming.domainLookupEnd

PerformanceTiming.domainLookupEnd 表示域名查询结束的 UNIX 时间戳。如果使用了持续连接,或者这个信息被存储到了缓存或本地资源,那么该值与 PerformanceTiming.fetchStart 相同。

(9) PerformanceTiming.connectStart

PerformanceTiming.connectStart 表示 HTTP 请求开始向服务器发送时的 UNIX 时间戳。如果使用持久连接,则该值与 PerformanceTiming.fetchStart 相同。

(10) PerformanceTiming.connectEnd

PerformanceTiming.connectEnd 表示浏览器与服务器之间的连接建立(即握手与认证等过程全部结束)的 UNIX 时间戳。如果使用持久连接,则该值与 PerformanceTiming.fetchStart 相同。

(11) PerformanceTiming.secureConnectionStart

PerformanceTiming.secureConnectionStart 表示浏览器与服务器开始安全链接的握手时的 UNIX 时间戳。如果当前网页不要求安全链接,该值为 0

(12) PerformanceTiming.requestStart

PerformanceTiming.requestStart 表示浏览器向服务器发送 HTTP 请求时的 UNIX 时间戳。

(13) PerformanceTiming.responseStart

PerformanceTiming.responseStart 表示浏览器从服务器收到(或从本地缓存读取)第一个字节时的 UNIX 时间戳。如果传输层从开始请求后失败并连接被重开,该值会被重置为新的请求的相应的时间。

(14) PerformanceTiming.responseEnd

PerformanceTiming.responseEnd 表示浏览器从服务器收到(或从本地缓存读取,或从本地资源读取)最后一个字节时(如果在此之前HTTP连接已经关闭,则返回关闭的时间)的 UNIX 时间戳。

(15) PerformanceTiming.domLoading

Performance.domLoading 表示当前网页 DOM 结构开始解析时(即 Document.readyState 属性变为 loading,相应的 readystatechange 事件触发时)的 UNIX 时间戳。

(16) PerformanceTiming.domInteractive

Performance.domInteractive 表示当前网页 DOM 结构解析结束,开始加载内嵌资源时(即 Document.readyState 的属性为 interactive,相应的 readystatechange 事件触发时)的 UNIX 时间戳。

(17) PerformanceTiming.domContentLoadedEventStart

PerformanceTiming.domContentLoadedEventStart 表示解析器触发 DomContentLoaded 事件,即所有需要被执行的脚本已经被解析时的 UNIX 时间戳。

(18) PerformanceTiming.domContentLoadedEventEnd

PerformanceTiming.domContentLoadedEventEnd 表示所有需要被执行的脚本均已被执行完成时的 UNIX 时间戳。

(19) PerformanceTiming.domComplete

PerformanceTiming.domComplete 表示文档解析完成,即 Document.readyState 变为 complete 且相应的 readystatechange 事件被触发时的 UNIX 时间戳。

(20) PerformanceTiming.loadEventStart

PerformanceTiming.loadEventStart 表示该文档下,load 事件被触发的 UNIX 时间戳。如果还未发送,值为 0

(21) PerformanceTiming.loadEventEnd

PerformanceTiming.loadEventEnd 表示该文档下,load 事件结束,即加载事件完成时的 UNIX 时间戳,如果事件未触发或未完成,值为 0

3. 各个性能时间节点

先看一张社区流传甚广的图片:

这张图描述了从用户开始路由到这个页面,到这个页面完全加载完成,总过经历的所有过程,根据图片,我们可以划分出各个有意义的考察性能的时间节点:

时间段描述
navigationStart ~ unloadEventEnd上一页面的卸载耗时
fetchStart ~ domainLookupStart查询 app DNS 缓存耗时
domainLookupStart ~ domainLookupEnddns 查询耗时
connectStart ~ connectEndTCP 连接耗时
connectEnd ~ secureConnectionStart针对 https 协议,在 tcp 握手后,传输真正的内容前,建立安全连接的耗时
fetchStart ~ responseStartTTFB(time to first byte), 即首包时长(从用户代理发出第一个请求开始,到页面读取到第一个字节的耗时)。在一个 web 程序中,用户代理发送的第一个 get 请求一般是 index.html,即接收到这个 html 文件的第一个字节的耗费时间
responseStart ~ responseEnd内容响应时长
domLoading ~ domInteractivedom 解析完成,即 DOM 树构建完成的时长
domContentLoadedEventEnd ~ loadEventStart渲染时长,domContentLoaded 表示 DOMCSSOM 均准备就绪(CSSOM 就绪意味着没有样式表阻止 js 脚本执行),开始构建渲染树
navigationStart ~ domLoadingFPT(first paint time), 即首次渲染时间,或白屏时间,从用户打开页面开始,到第一次渲染出可见元素为止
navigationStart ~ domInteractiveTTI(time to interact),首次可交互时间
fetchStart ~ domContentLoadedEventEndhtml 加载完成时间,此时 DOM 已经解析完成
navigationStart ~ loadEventStart页面完全加载完成的时间

三 PerformanceEntry

PerformanceTiming 是向下兼容的旧接口,且只能保存页面打开过程(从路由到该地址,到当前页面load事件触发完成)中的性能数据,如果需要在页面运行过程中持续获取性能数据,需要使用 PerformanceEntry 实例。

1. Performance.getEntries

Performance.getEntries([filterOption]) 方法用于获取 PerformanceEntry 实例数组,它接受一个可选的参数,如果不传则获取全部的 entry 数据。该参数对象可以包含以下,属性:

  • name : performance entry 的名字
  • entryType : entry 类型,合法的 entry 值可以从 PerformanceEntry.entryType
  • initiatorType : 初始化资源的类型,取值由 PerformanceResourceTiming.initiatorType 接口定义

该方法返回 entries 数组,包含了所有符合筛选条件的 entry

window.performance.getEntries({ entryType: 'resource' })
/*
0: PerformanceNavigationTiming {unloadEventStart: 0, unloadEventEnd: 0, domInteractive: 1988.2200000574812, domContentLoadedEventStart: 2038.9849999919534, domContentLoadedEventEnd: 2049.5600000722334, …}
1: PerformanceResourceTiming {initiatorType: "link", nextHopProtocol: "h2", workerStart: 0, redirectStart: 0, redirectEnd: 0, …}
...
13: PerformancePaintTiming {name: "first-paint", entryType: "paint", startTime: 1987.154999980703, duration: 0}
*/

2. Performance.getEntriesByName

Performance.getEntriesByName(name [, type]) 方法接受两个参数:

  1. name : 必选,这个 entry 的名称
  2. type: 可选,想要过滤的 entry 类型

返回符合条件的 entries 数组。

window.performance.mark('_my_mark_1')
window.performance.getEntriesByName('_my_mark_1')
// [PerformanceMark]
// 0: PerformanceMark {detail: null, name: "_my_mark_1", entryType: "mark", startTime: 5614367.25000001, duration: 0}

3. Performance.getEntriesByType

Performance.getEntriesByType(type) 方法接受一个 entryType ,返回符合类型的 PerformanceEntry 列表。

window.performance.getEntriesByType('paint')
/*
[PerformancePaintTiming, PerformancePaintTiming]
0: PerformancePaintTiming {name: "first-paint", entryType: "paint", startTime: 2775.984999956563, duration: 0}
1: PerformancePaintTiming {name: "first-contentful-paint", entryType: "paint", startTime: 2775.984999956563, duration: 0}
*/

4. PerformanceEntry 属性

PerformanceEntry 的所有属性都是只读的

(1) PerformanceEntry.entryType,PerformanceEntry.name

PerformanceEntry.entryType 是一个 performance entry 的类型,值为 DOMString 。用于区分不同类型的 performance 数据。PerformanceEntry.name 标识当前性能数据块的名称,它允许的值和类型随着 entryType 不同而不同

标识的性能时间类型name 属性类型name 属性含义
frame, navigationPerformanceFrameTiming, PerformanceNavigationTimingURL产生这两个performance数据的文档地址
resourcePerformanceResourceTimingURL请求的资源的地址,即便请求被重定向了,这个值也不会改变
markPerformanceMarkDOMString创建该 mark 时指定的 name 属性
measurePerformanceMeasureDOMString使用 performance.measure() 创建 measure 时提供的 name 属性
paintPerformancePaintTimingDOMStringfirst-paint 或者 first-contentful-paint

(2) PeformanceEntry.startTime

performanceEntry.startTime 表示这个 performance 数据被创建时的时间戳。根据不同的 entryType 值,startTime 取值的含义有些不同:

  • frame : 指定的帧(frame)开始渲染时的时间
  • mark : 该 mark 标记被创建时的时间戳(performance.mark() 调用时的时间)
  • measure : 该 measure 被创建时的时间戳(performance.measure() 方法调用时的时间)
  • navigation : 值为 0
  • resource : 浏览器发起该资源请求时的时间戳

(3) PerformanceEntry.duration

performanceEntry.duration 表示这个 performance 数据标识的持续时间。根据不同的 entryType 值,duration 取值的含义有些不同:

  • frame : 两个连续的帧开始渲染的时间之差
  • mark : 值为 0,mark 标记没有持续时间
  • measure : 要测量的 measure 的持续时间
  • navigation : performanceEntry.startTimeperformanceNavigationTiming.loadEventEnd 的差值
  • resource : 该资源加载的耗时,即 performanceEntry.startTimeperformanceResourseTiming.responseEnd 的差值

四 手动测量性能

Performance 接口除了测量资源加载性能(PerformanceResourceTiming),渲染帧性能(PerformanceFrameTiming),页面初始化性能(PerformanceNavigationTiming)等等以外,还允许手动创建标记,让开发者在代码中测量程序执行的性能。

1. Performance.mark

performance.mark(name) 方法接受一个字符串,用于创建一个标记(mark),字符串即为这个标记的名称,这个标记会固定下创建时的时间戳。后续开发者可以通过不同标记的 startTime 值相减来获取一段代码的执行耗时,也可以通过 performance.measure 来计算耗时。该方法返回一个 mark 类型的 PerformanceEntry 实例。

一个标记(mark)具有以下属性值:

  • entryType : 固定为 mark
  • name : 创建标记时通过参数指定的标记名
  • startTime : 创建 mark 时的时间戳
  • duration : 0,mark 没有持续时间
window.performance.mark('_test_mark_1')
// PerformanceMark {detail: null, name: "_test_mark_1", entryType: "mark", startTime: 1978026.354999999, duration: 0}

window.performance.mark('_test_mark_2')
// PerformanceMark {detail: null, name: "_test_mark_2", entryType: "mark", startTime: 1991866.0800000008, duration: 0}

window.performance.mark('_test_mark_1')
// PerformanceMark {detail: null, name: "_test_mark_1", entryType: "mark", startTime: 1994302.8449999993, duration: 0}

performance.getEntriesByName('_test_mark_1')
// [PerformanceMark, PerformanceMark]

mark 的 name 不要求唯一

2. Performance.measure

performance.measure(name [, startMarkName [, endMarkName]]) 方法创建一个 entryType 类型为 measure 的 PerformanceEntry 对象,用于计算两个标志位之间的间隔时间。它接受三个参数:

  1. name : 创建 measure 时指定的名称
  2. startMarkName : 可选,指定作为开始时间点的 mark 的名称
  3. endMarkName : 可选,指定作为结束时间点的 mark 的名称

该方法返回一个 measure 类型的 PerformanceEntry ,它具有以下属性

  • entryType : 值为 measure
  • name : 创建 measure 时的名称
  • startTime : 这一段测量的开始时间,如果传了第二个参数,那就是该 mark 的标记时间
  • duration : 这一段测量持续的时间
window.performance.measure('__measure_test_mark_1', '_test_mark_1')
// PerformanceMeasure {detail: null, name: "__measure_test_mark_1", entryType: "measure", startTime: 1994302.8449999993, duration: 476087.44500000123}

window.performance.measure('__measure_test_mark', '_test_mark_1', '_test_mark_2')
PerformanceMeasure {detail: null, name: "__measure_test_mark", entryType: "measure", startTime: 1994302.8449999993, duration: -2436.7649999985006}

window.performance.measure('__measure_test_mark')
PerformanceMeasure {detail: null, name: "__measure_test_mark", entryType: "measure", startTime: 0, duration: 3290800.8249999993}

综上, duration 取值有三种情况

  1. 调用 performance.measure 创建 measure 时,三个参数都传递了,即 name, startMark, endMark 同时存在,那么 duration = startMark.startTime - endMark.startTime (注意,并不是 endMark - startMark),duration 此时可能是负值
  2. 创建 measure 只传了两个参数,即 endMark 不存在,那么该 measure 计算的是从 startMark.startTime 到创建 measure 时的时间间隔
  3. 创建 measure 只提供了一个参数,即 startMark, endMark 都不存在,那么该 measure 计算的是从页面打开到创建 measure 时的时间间隔

3. 清理 mark,measure

  1. Performance.clearMark([name]) 方法用于清理指定的 mark,如果不提供参数,则所有 entryType 为 markPerformanceEntry 全部被清理。
  2. Performance.clearMeasure([name]) 方法用于清理指定的 measure,如果不提供参数,则所有 entryType 为 measurePerformanceEntry 全部被清理。
performance.clearMarks('_test_mark_1')
undefined
performance.getEntriesByType('mark')
// [PerformanceMark, PerformanceMark]
// PerformanceMark {detail: null, name: "LUX_end", entryType: "mark", startTime: 2039.1650000001391, duration: 0}
// PerformanceMark {detail: null, name: "_test_mark_2", entryType: "mark", startTime: 1991866.0800000008, duration: 0}