目前前端性能监控系统大体为分两类:以GA为表明的代码监控和以webpagetest为表明的工具监控。javascript
代码监控依托于js代码并部署到需监控的页面,手动计算时间差或者使用浏览器的的API进行数据统计。php
影响代码监控数据的因素有如下几种:css
工具监控不用将统计代码部署到页面中,通常依托于虚拟机。以webpageTest为例,输入需统计的url而且选择运行次url的浏览器版本,webpageTest后台虚拟机对url进行请求分析后即可以给出各类性能指标,好比瀑布流、静态文件数量、首屏渲染时间等等。html
代码监控和工具监控的对好比下表:前端
根据目前业务需求以及成本预算,最终决定采用代码监控方案。如下分别介绍代码监控各方面的实现细节。html5
前端性能统计的数据大体有如下几个:java
下面介绍几种以上几个数据的统计方案。jquery
使用注入代码监控的方式统计以上指标,在没有一些浏览器新API(以下文将提到的timing API)的支持下,获得的数据大都是估值,虽然不许确,但也有必定的参考价值。web
白屏时间节点指的是从用户进入网站(输入url、刷新、跳转等方式)的时刻开始计算,一直到页面有内容展现出来的时间节点。这个过程包括dns查询、创建tcp链接、发送首个http请求(若是使用https还要介入TLS的验证时间)、返回html文档、html文档head解析完毕。django
使用注入代码监控没法获取解析html文档以前的时间信息,目前广泛使用的白屏时间统计方案是在html文档的head中全部的静态资源以及内嵌脚本/样式以前记录一个时间点,在head最底部记录另外一个时间点,二者的差值做为白屏时间。以下:
<html> <head> <meta charset="UTF-8"/> <!--这里还有一大串meta信息--> <script> var start_time = new Date();//统计起点,实际为html开始解析的时间节点 </script> <link href='a.css'></link> <script src='a.js'></script> <script> var end_time = new Date();//统计起点,实际为html开始解析的时间节点 </script> </head> <body> </body> </html>
上述代码中的end_time
和start_time
的差值通常做为白屏时间的估值,但理论上来说,这个差值只是浏览器解析html文档head的时间,并不是准确的白屏时间。
首屏时间的统计比较复杂,目前应用比较广的方案是将首屏的图片、iframe等资源添加onload事件,获取最慢的一个。
这种方案比较适合首屏元素数量固定的页面,好比移动端首屏不论屏幕大小都展现相同数量的内容,响应式得改变内容的字体、尺寸等。可是对于首屏元素不固定的页面,这种方案并不适用,最典型的就是PC端页面,不一样屏幕尺寸下展现的首屏内容不一样。上述方案便不适用于此场景。
用户可操做的时间节点即dom ready触发的时间,使用jquery能够经过$(document).ready()
获取此数据,若是不使用jQuery能够参考这里经过原生方法实现dom ready。
总下载时间即window.onload
触发的时间节点。
目前大多数web产品都有异步加载的内容,好比图片的lazyload等。若是总下载时间须要统计到这些数据,能够借鉴AOP的理念,在请求异步内容以前和以后分别打点,最后计算差值。不过一般来说,咱们说的总下载时间并不包括异步加载的内容。
window.performance
APIwindow.performance
是W3C性能小组引入的新的API,目前IE9以上的浏览器都支持。一个performance对象的完整结构以下图所示:
memory
字段表明JavaScript对内存的占用。
navigation
字段统计的是一些网页导航相关的数据:
redirectCount
:重定向的数量(只读),可是这个接口有同源策略限制,即仅能检测同源的重定向;最重要的是timing
字段的统计数据,它包含了网络、解析等一系列的时间数据。
timing
APItiming
的总体结构以下图所示:
各字段的含义以下:
startTime
:有些浏览器实现为navigationStart
,表明浏览器开始unload前一个页面文档的开始时间节点。好比咱们当前正在浏览baidu.com,在地址栏输入google.com并回车,浏览器的执行动做依次为:unload当前文档(即baidu.com)->请求下一文档(即google.com)。navigationStart的值即是触发unload当前文档的时间节点。
若是当前文档为空,则navigationStart的值等于fetchStart。
redirectStart
和redirectEnd
:若是页面是由redirect而来,则redirectStart和redirectEnd分别表明redirect开始和结束的时间节点;unloadEventStart
和unloadEventEnd
:若是前一个文档和请求的文档是同一个域的,则unloadEventStart
和unloadEventEnd
分别表明浏览器unload前一个文档的开始和结束时间节点。不然二者都等于0;fetchStart
是指在浏览器发起任何请求以前的时间值。在fetchStart和domainLookupStart
之间,浏览器会检查当前文档的缓存;domainLookupStart
和domainLookupEnd
分别表明DNS查询的开始和结束时间节点。若是浏览器没有进行DNS查询(好比使用了cache),则二者的值都等于fetchStart
;connectStart
和connectEnd
分别表明TCP创建链接和链接成功的时间节点。若是浏览器没有进行TCP链接(好比使用持久化链接webscoket),则二者都等于domainLookupEnd
;secureConnectionStart
:可选。若是页面使用HTTPS,它的值是安全链接握手以前的时刻。若是该属性不可用,则返回undefined。若是该属性可用,但没有使用HTTPS,则返回0;requestStart
表明浏览器发起请求的时间节点,请求的方式能够是请求服务器、缓存、本地资源等;responseStart
和responseEnd
分别表明浏览器收到从服务器端(或缓存、本地资源)响应回的第一个字节和最后一个字节数据的时刻;domLoading
表明浏览器开始解析html文档的时间节点。咱们知道IE浏览器下的document有readyState
属性,domLoading
的值就等于readyState
改变为loading
的时间节点;domInteractive
表明浏览器解析html文档的状态为interactive
时的时间节点。domInteractive
并不是DOMReady,它早于DOMReady触发,表明html文档解析完毕(即dom tree建立完成)可是内嵌资源(好比外链css、js等)还未加载的时间点;domContentLoadedEventStart
:表明DOMContentLoaded
事件触发的时间节点:
页面文档彻底加载并解析完毕以后,会触发DOMContentLoaded事件,HTML文档不会等待样式文件,图片文件,子框架页面的加载(load事件能够用来检测HTML页面是否彻底加载完毕(fully-loaded))。
domContentLoadedEventEnd
:表明DOMContentLoaded
事件完成的时间节点,此刻用户能够对页面进行操做,也就是jQuery中的domready时间;domComplete
:html文档彻底解析完毕的时间节点;loadEventStart
和loadEventEnd
分别表明onload事件触发和结束的时间节点
可使用Navigation.timing
统计到的时间数据来计算一些页面性能指标,好比DNS查询耗时、白屏时间、domready等等。以下:
Resource timing API是用来统计静态资源相关的时间信息,详细的内容请参考W3C Resource timing。这里咱们只介绍performance.getEntries
方法,它能够获取页面中每一个静态资源的请求,以下:
能够看到performance.getEntries
返回一个数组,数组的每一个元素表明对应的静态资源的信息,好比上图展现的第一个元素对应的资源类型initiatorType
是图片img
,请求花费的时间就是duration
的值。
关于Resource timing API的使用场景,感兴趣的同窗能够深刻研究。
// 计算加载时间 function getPerformanceTiming () { var performance = window.performance; if (!performance) { // 当前浏览器不支持 console.log('你的浏览器不支持 performance 接口'); return; } var t = performance.timing; var times = {}; //【重要】页面加载完成的时间 //【缘由】这几乎表明了用户等待页面可用的时间 times.loadPage = t.loadEventEnd - t.navigationStart; //【重要】解析 DOM 树结构的时间 //【缘由】检讨下你的 DOM 树嵌套是否是太多了! times.domReady = t.domComplete - t.responseEnd; //【重要】重定向的时间 //【缘由】拒绝重定向!好比,http://example.com/ 就不应写成 http://example.com times.redirect = t.redirectEnd - t.redirectStart; //【重要】DNS 查询时间 //【缘由】DNS 预加载作了么?页面内是否是使用了太多不一样的域名致使域名查询的时间太长? // 可以使用 HTML5 Prefetch 预查询 DNS ,见:[HTML5 prefetch](http://segmentfault.com/a/1190000000633364) times.lookupDomain = t.domainLookupEnd - t.domainLookupStart; //【重要】读取页面第一个字节的时间 //【缘由】这能够理解为用户拿到你的资源占用的时间,加异地机房了么,加CDN 处理了么?加带宽了么?加 CPU 运算速度了么? // TTFB 即 Time To First Byte 的意思 // 维基百科:https://en.wikipedia.org/wiki/Time_To_First_Byte times.ttfb = t.responseStart - t.navigationStart; //【重要】内容加载完成的时间 //【缘由】页面内容通过 gzip 压缩了么,静态资源 css/js 等压缩了么? times.request = t.responseEnd - t.requestStart; //【重要】执行 onload 回调函数的时间 //【缘由】是否太多没必要要的操做都放到 onload 回调函数里执行了,考虑过延迟加载、按需加载的策略么? times.loadEvent = t.loadEventEnd - t.loadEventStart; // DNS 缓存时间 times.appcache = t.domainLookupStart - t.fetchStart; // 卸载页面的时间 times.unloadEvent = t.unloadEventEnd - t.unloadEventStart; // TCP 创建链接完成握手的时间 times.connect = t.connectEnd - t.connectStart; return times; }
JavaScript异常通常有两方面:语法错误和运行时错误。两种错误的捕获和处理方式不一样,从而影响具体的方案选型。一般来讲,处理JS异常的方案有两种:try...catch
捕获 和 window.onerror
捕获。如下就两种方案分别分析各自的优劣。
虽然语法错误本应该在开发构建阶段使用测试工具避免,但不免会有马失前蹄部署到线上的时候。
try...catch
捕获这种方案要求开发人员在编写代码的时候,在预估有异常发生的代码段使用try...catch
,在发生异常时将异常信息发送给接口:
try{ //可能发生异常的代码段 }catch(e){ //将异常信息发送服务端 }
try...catch
的优势是能够细化到每一个代码块,而且能够自定义错误信息以便统计。
具体到上文提到的两种js异常,try...catch
没法捕获语法错误,当遇到语法错误时,浏览器仍然会抛出错误Uncaught SyntaxError
,可是不会被捕获,不会走进catch的代码块内。
另外,若是try代码块中有回调函数也不会被捕获,好比:
try{ var btn = $('#btn'); btn.on('click',function(){ //throw error }); }catch(e){}
上述代码中btn的监听函数里抛出的异常没法被外层的catch捕获到,必须额外套一层:
try{ var btn = $('#btn'); btn.on('click',function(){ try{ //throw error }catch(e){} }); }catch(e){}
综上所述,try...catch
方案的部署很是复杂,若是人工部署除了要求巨量的工做量,还跟开发人员的能力和经验有关。若是依赖编译工具部署(好比fis),那每一个代码块都套一层try...catch
也是很是难看的而且容易引起一些不可预估的问题。
window.onerror
捕获这种方式不须要开发人员在代码中书写大量的try...catch
,经过给window添加onerror监听,在js发生异常的时候即可以捕获到错误信息,语法异常和运行异常都可被捕获到。可是window.onerror
这个监听必须放在全部js文件以前才能够保证可以捕获到全部的异常信息。
window.onerror
事件的详细信息参考这里。
/** * @param {String} errorMessage 错误信息 * @param {String} scriptURL 出错文件的URL * @param {Long} lineNumber 出错代码的行号 * @param {Long} columnNumber 出错代码的列号 * @param {Object} errorObj 错误信息Object */ window.onerror = function(errorMessage, scriptURL, lineNumber,columnNumber,errorObj) { // code.. }
onerror的实现方式各浏览器略有差别,可是前三个参数都是相同的,某些低版本浏览器没有后两个参数。
最后一个参数errorObj各浏览器实现的程度不一致,具体可参考这里。
下图是被onerror捕获到的一个异常的具体信息:
综上所述,window.onerror
方案的优势是减小了开发人员的工做量,部署方便,而且能够捕获语法错误和运行错误。缺点是错误信息不能自定义,而且errorObj每种浏览器的实现有略微差别,致使需统计的信息有局限性。
为了提升web性能,目前大部分web产品架构中都有CDN这一环,将资源部署到不一样的域名上,充分利用浏览器的并发请求机制。那么在跨域JS文件中发生异常的时候,onerror监听会捕获到什么信息呢?请看下图:
只有一个稍微有价值的信息Script error
,其余什么信息都没有,为何会这样呢?
咱们都知道浏览器有同源资源限制,常规状态下是没法进行跨域请求的。而script、img、iframe标签的src属性是没有这种限制的,这也是不少跨域方案的基础。可是即便script标签能够请求到异域的js文件,此文件中的信息也并不能暴露到当前域内,这也是浏览器的安全措施所致。
那么有没有办法获取到异域资源的异常信息呢?
其实很简单,目前能够说基本上全部的web产品对于js/css/image等静态资源都在服务端设置了Access-Control-Allow-Origin: *
的响应头,也就是容许跨域请求。在这个环境下,只要咱们在请求跨域资源的script标签上添加一个crossorigin
属性便可:
<script src="http://static.toutiao.com/test.js" crossorigin></script>
这样的话,异域的test.js文件中发生异常时即可以被当前域的onerror监听捕获到详细的异常信息。