H5性能监控「Performance」

性能监控 在前端一直是一个口头上备受关注但开发中又常被忽略的点,毕竟不是每个开发者很容易就做到的事。好在HTML5新增了performance特性,它是High Resolution Time API 的一部分,目的在于获取到当前页面中与性能相关的信息,以便帮助开发者直观感受页面性能及针对问题优化。
  了解如何监控页面性能前,我们先回顾几个指标:
(1)白屏时间:页面被打开,到首字节渲染呈现所需的时间。
(2)首屏时间:首屏内容渲染完成所需的时间。
(3)下载时间(HTTP请求耗时):页面所需资源从服务器上下载完成所需的时间。
(4)DOM树解析时间:资源下载完成到页面构建展示出来所需的时间。
  ...
  这些信息都如何获取?在此标准之前,也有一些手段可以实现,但H5的performance直接来源于浏览器,与手工Date.time,Cookie等对比,使用上更方便,数据上更准确。(Date.now()会受程序阻塞影响)


$ Performance 属性

  关于performance属性,建议读者自己在工具编辑器上直接打印出来看看更能真切的体会该接口。本文主要介绍前两者,对其他内容感兴趣的同学,可以 戳这里

  1. .timing(只读):对象;包含了延迟相关的性能信息。
  2. .navigation(只读):对象;包含了指定的时间段里发生的操作相关信息,包括页面是加载还是刷新、发生了多少次重定向等等。
  3. .timeOrigin(只读):即将失效。用于返回性能测量开始时的高精度时间戳。
  4. .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样式加载,如backgroundurl()资源
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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,123评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,031评论 2 384
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,723评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,357评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,412评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,760评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,904评论 3 405
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,672评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,118评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,456评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,599评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,264评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,857评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,731评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,956评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,286评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,465评论 2 348

推荐阅读更多精彩内容