性能监控 在前端一直是一个口头上备受关注但开发中又常被忽略的点,毕竟不是每个开发者很容易就做到的事。好在HTML5新增了performance
特性,它是High Resolution Time API 的一部分,目的在于获取到当前页面中与性能相关的信息,以便帮助开发者直观感受页面性能及针对问题优化。
了解如何监控页面性能前,我们先回顾几个指标:
(1)白屏时间:页面被打开,到首字节渲染呈现所需的时间。
(2)首屏时间:首屏内容渲染完成所需的时间。
(3)下载时间(HTTP请求耗时):页面所需资源从服务器上下载完成所需的时间。
(4)DOM树解析时间:资源下载完成到页面构建展示出来所需的时间。
...
这些信息都如何获取?在此标准之前,也有一些手段可以实现,但H5的performance
直接来源于浏览器,与手工Date.time,Cookie等对比,使用上更方便,数据上更准确。(Date.now()
会受程序阻塞影响)
$ Performance 属性
关于performance属性,建议读者自己在工具编辑器上直接打印出来看看更能真切的体会该接口。本文主要介绍前两者,对其他内容感兴趣的同学,可以 戳这里
-
.timing
(只读)
:对象;包含了延迟相关的性能信息。 -
.navigation
(只读)
:对象;包含了指定的时间段里发生的操作相关信息,包括页面是加载还是刷新、发生了多少次重定向等等。 -
.timeOrigin
(只读)
:即将失效。用于返回性能测量开始时的高精度时间戳。 -
.memory:由chrome拓展的非标准属性,用于返回基本内存的使用情况。注意非chrome不支持。
# Performance.timing 只读
const PerformanceTiming = window.performance.timing
返回值为一个对象,记录着完整的页面加载信息。其各个节点如下:
看着上图,回顾一下一般意义的页面加载过程:浏览器向服务器请求资源
--> DOM结构解析
--> 构建DOM树
--> 构建CSS规则树
--> 构建渲染树
--> 绘制页面
。可以看出,这个过程只是上图中的某一小部分,我们来详谈一下实际的整个过程
-
Prompt for unload 阶段
-
.navigationStart
:浏览器完成卸载前一个文档的时间。如果没前一个文档,则该值与第三步.fetchStart
的值相同。 -
.unloadEventStart
:返回前一个同源文档出发卸载(unload)事件前的时间。如果没有前一个文档,或前文档与本文档不同源,或需重定向,则返回0。 -
.unloadEventEnd
:返回前一个同源文档完成卸载的时间。如果没有或文档不同源,则返回0.
-
-
Redirect 阶段
-
.redirectStart
:http重定向开始的时间。如果中间有多个重定向,且每个重定向均同源,则返回第一个重定向的.fetchStart
时间,若不同源,则为0 -
.redirectEnd
:http重定向结束时间。如果中间有多个重定向且均同源,则返回最后一个重定向结束时间。若不同源,则为0。
-
-
App cache 阶段
-
.fetchStart
:浏览器准备好使用HTTP请求来获取(fetch
)文档的时间,这个时间会在检查任何应用缓存之前。
-
-
DNS查询阶段
-
.domainLookupStart
:用户代理对当前文档所属域进行DNS查询开始的时间。如果是长连接(如websocket
),或本地缓存了,则该值与.fetchStart
相同 -
.domainLookupEnd
:域名查询结束的时间。如果是长连接,或本地缓存了,则该值与.fetchStart
相同
-
-
TCP连接阶段
-
.connectStart
:用户代理开始向服务器请求所需文档时,连接建立的开始时间。如果是长连接,或本地缓存了,则该值与.fetchStart
相同 -
.secureConnectStart
:返回与服务器开始SSL握手时的时间。异常情况同上。 -
.connectEnd
: HTTP握手成功,认证结束,连接建立时的时间。如果是长连接,或本地缓存了,则该值与.fetchStart
相同。
-
-
Request 阶段
-
requestStart
:从服务器/缓存/本地资源中开始请求文档的时间。如果连接发生断开重连,该信息会被刷新。 - 没有请求结束时间是因为该动作发生在服务器端,且受数据链路等各个因素影响,浏览器并不能准确反馈该信息
-
-
Response 阶段
-
.responseStart
:从服务器/缓存/本地资源中接收到第一个字节时的时间。如果连接发生断开重连,该信息会被刷新。 -
.responseEnd
:从服务器/缓存/本地资源中接收到最后一个字节时的时间。如果连接提前关闭,则返回提前关闭的时间。获取该值时需注意要在Response结束之后,如window.onload
,否则可能不准确。
-
-
Processing 执行阶段
-
.domLoading
:资源下载完成,开始解析DOM结构,当Document.readyState
的值更新为loading
时的时间。 -
.domInteractive
:DOM解析完成,开始加载内嵌资源,即Document.readyState
的值更新为interactive
时的时间 - 执行阶段内的 DOMContentLoaded 阶段
-
.domContentLoadedEventStart
:解析器发送DOMContentLoaded
事件,所有需要被执行的脚本均解析完成时的时间。 -
.domContentLoadedEventEnd
:所有立即执行的脚本均执行完成时的时间。不执行的脚本如懒加载资源不在该范围内。
-
-
.domComplete
:当前文档解析完成,document.readyState
的值更新为complete
时的时间。
-
-
load 业务涉入阶段
-
.loadEventStart
:文档触发load事件的时间,如果还没触发,则返回0。 -
.loadEventEnd
:文档结束load事件的时间,未触发则返回0。
-
# 性能监控指标
通过以上的各个事件分析,不难得出如下各个时间段:
const timing = window.performance.timing
-
DNS解析耗时:
timing.domainLookupEnd - timing.domainLookupStart
-
TCP连接耗时:
timing.connectEnd - timing.connectStart
-
发送请求耗时:
timing.responseStart - timing.requestStart
-
接收请求耗时:
timing.responseEnd - timing.responseStart
-
解析DOM耗时:
timing.domInteractive - timing.domLoading
-
页面加载完成:
timing.domContentLoadedEventStart - timing.domInteractive
-
DOMContentLoaded事件耗时:
timing.domContentLoadedEventEnd - timing.domContentLoadedEventStart
-
DOM加载完成:
timing.domComplete - timing.domContentLoadedEventEnd
-
DOMLoad事件耗时:
timing.loadEventEnd - timing.loadEventStart
除此之外,在文首提到的其他几个性能指标,如下:
-
白屏时间:
timing.responseStart - timing.navigationStart
-
首屏时间:
timing.domComplete- timing.navigationStart
-
资源下载总耗时:
timing.responseEnd - timing.requestStart;
-
请求完毕至DOM加载:
timing.domInteractive - timing.responseEnd
# 实战案例
封装一个函数如下,注释前半部为参数功能,后半部为监控到页面性能问题时可能的原因
function getPerformanceTiming () {
var performance = window.performance
// 浏览器兼容性考虑
if(!performance) {
console.log('您的浏览器不支持 performance 接口')
return
}
const t = performance.timing
let times = {}
// 页面加载完成时间 - 用户需等待页面可用时间
times.loadPage = t.loadEventEnd - t.navigationStart
// 解析dom树结构时间 - DOM树嵌套不宜太深
times.domReady = t.domeComplete - t.responseEnd
// 重定向时间 - 若拒绝重定向,检查是否有类似‘http://example.com/’写成‘http://example.com’错误
times.redirect = t.redirectEnd - redirectStart
// DNS解析时间 - 可增加DNS预加载。页面涉及域名是否过多
times.lookupDomian = t.domainLookupEnd - t.domainLookupStart
// 首字节响应时间 - 数据链路的响应速度,受机房,CDN,带宽,服务器性能等影响
times.ttfb = t.responseStart - t.navigationStart
// 资源加载完成时间 - Nginx上配置gzip压缩减少下载资源
times.request = t.responseEnd - t.reuqestStart
// onload执行效率 - 避免过多逻辑在onload中执行,考虑资源懒加载,延迟获取等
times.loadEvent = t.loadEventEnd - t.loadEventStart
// DNS缓存时间
times.appcache = t.domianLookupStart - t.fetchStart
// 卸载页面时间
times.unloadEvent = t.unloadEventEnd - t.unloadEventStart
// TCP连接建立及完成握手时间
times.connect = t.connectEnd - t.connectStart
return times
}
# Performance.navigation 只读
.navigation
返回一个performanceNavigation对象,提供了在指定的时间段里发生的操作和相关信息,包括页面是加载、刷新还是重定向。
const navigation = window.performance.navigation
该对象返回值信息如下
-
页面载入类型 -
type
-
0
:同TYPE_NAVIGATE
;如点击链接,url输入,脚本执行跳转,或书签和表单的提交等方式载入 -
1
:同TYPE_RELOAD
;如点击刷新页面按钮,或脚本Location.reload()
载入 -
2
:同TYPE_BACK_FORWARD
;通过历史记录的前进和后退进入 -
255
:同TYPE_RESERVED
;通过其他方式进入
-
- 重定向次数 -
redirectCount
-
序列化方法 -
toJson()
链式调用办法,将PerformanceNavigation转化为JSON对象。
$ Performance 方法
timing
属性主要针对文档载入及之前的各个节点性能监控,无法落实到其他业务逻辑执行。想要监控更多信息,就需要使用Performance接口提供的方法来实现。
# now() (单位ms)
performance.now()
方法返回了相对于 performance.timing.navigationStart
(页面初始化) 的时间,而Date.now()
返回的是UNIX时间也就是距1970年的时间。且因为performance.now()
的时间是以一定速率慢慢增加的,不受系统时间影响,也不受进程阻塞影响,比Date.now()
时间来的更精准一些。
let t0 = window.performance.now();
todo()
let t1 = window.performance.now();
console.log("todo执行时间:", (t1 - t0) + "毫秒.")
# getEntries()
返回一个按startTime
排序的数组,包含加载本页面所有的资源请求相关时间数据的集合。为更好的理解看一个entry实例数据,以访问https://www.baidu.com/
为例:
const entries = window.performance.getEntries()
console.log(entries)
以下为返回数组的第一项:
可以发现,整个 Performance.timing 的数据节点均已包含。除此之外,还包括了以下几个信息:
-
name:资源名称。是资源的绝对路径或
mark()
方法自定义的名称。 - startTime:开始时间
- duration:加载时间
- entryType:资源类型;详情如下
- initiatorType:请求发起者;详情如下
entryType的值
值 | 描述 |
---|---|
mark |
通过mark()添加到数组中的对象 |
measure |
通过measure()添加到数组中的对象 |
resource |
所有资源加载时间(重要) |
navigation |
导航相关信息,仅chrome和Opera支持 |
frame |
- |
server |
- |
initiatorType的值
值 | 发起对象 | 描述 |
---|---|---|
link/script/img/iframe 等 |
某个标签元素 | 标签形式加载 |
css |
某个css样式 | 通过css样式加载,如background 的url() 资源 |
xmlhttprequest |
某个http请求 | 通过xhr加载的资源 |
navigation |
某个performanceNavigation对象 | 当对象是PerformanceNavigationTiming时返回 |
因此,我们获取其性能时间数据可封装函数如下
// 计算加载时间
function getEntryTiming (entry) {
var t = entry;
var times = {};
// 重定向的时间
times.redirect = t.redirectEnd - t.redirectStart;
// DNS 查询时间
times.lookupDomain = t.domainLookupEnd - t.domainLookupStart;
// 内容加载完成的时间
times.request = t.responseEnd - t.requestStart;
// TCP 建立连接完成握手的时间
times.connect = t.connectEnd - t.connectStart;
// 挂载 entry 返回
times.name = entry.name;
times.entryType = entry.entryType;
times.initiatorType = entry.initiatorType;
times.duration = entry.duration;
return times;
}
// run it
var entries = window.performance.getEntries();
entries.forEach(function (entry) {
var times = getEntryTiming(entry);
console.log(times);
});
执行该方法会发现,一个全量的entries
存在了过多的干扰信息,如果要从中挑出某些有用项进行比较只能通过数组过滤手段实现比较麻烦,好在performance接口提供了这个方法
# getEntriesByType()
performance.getEntriesByType()
方法返回给定类型的entries
数组集合,其本质就是在全量数据中按entryType
属性过滤,返回过滤后的数据,效果等同于Array.filter()
。该方法常配合mark()
方法使用,用来获取用户自己打的标签数据。
entries = window.performance.getEntriesByType(type);
# getEntriesByName()
使用办法同getEntriesByType()
,接受一个参数,用于指定entries
名称。可以用来统计某一个函数被执行的次数及各个执行时刻,另一个更重要的是用来检索measure测量的duration耗时。
# mark()
使用performance.mark()
也可以精准的计算程序的执行时间。思路就是在某些关键位置插入一些标记,当程序运行到标记处时,Performance会入栈一个entry
。这样,通过在需要分析性能的逻辑段落前后插入不同的标记,来实现对该处性能的监控。
function markSample(name) {
const markStart = name + '_markStart'
const markEnd= name + '_markEnd'
window.performance.mark(markStart)
for(let i = 0; i < 100; i++) {
for(let j = 0; j < 100; j++) {
// TODO:
}
}
window.performance.mark(markEnd)
}
// run it
markSample(‘first’)
const marks = window.performance.getEntriesByType('mark')
console.log(marks)
执行结果会包含四个关键属性,如下:
# measure()
performance.measure()
用于测量两个标记之间执行的时间,并把它赋值给第一个参数(measure名称)上。如在上例的markSample
函数底部插入一下代码
window.performance.measure('measure_test', markStart, markEnd)
var measureTest= window.performance.getEntriesByName('measure_test');
console.log(measureTest);
值得关注的是,由于标记在插入后,每次程序执行到此处将入栈一个entry
,而该数据是记录在全局的window
下的,因此当标记过多或被执行次数太多时,可能出现内存污染等问题,因此,这就要求在标记使用结束后及时清除他们。
# clearMarks()
performance.clearMarks()
接受 0/1 个参数,表示将要清除的标记名称
// 指定清除某个标记
window.performance.clearMarks('first_markStart')
// 清除所有标记
window.performance.clearMarks()
# clearMeasures()
测量完成后也应当及时清除,用法:
// 清除指定测量
window.performance.clearMeasures('first_measure');
// 清除所有测量
window.performance.clearMeasures();
$ 使用mark测量timing事件
可能有个错误的理解就是performance.measure()
只能测量performance.mark()
的标记,其实不然,比如,在timing中,我们是这么测量domReady事件的:
cosnt t = performance.timing
const domReady = t.domComplete - t.responseEnd;
console.log(domReady )
也可以使用measure()
来实现如下:
window.performance.measure('domReady','responseEnd' , 'domComplete');
var domReadyMeasure = window.performance.getEntriesByName('domReady');
console.log(domReadyMeasure);
$ refs
参考文献
performance - MDN
HTML5 performance API 草案.
初探performance - AlloyTeam