对于现在的互联网体验来说,前端的工作已经不再是单一的按照原型图切出页面就算完事了,它还承载着后续用户在使用中的体验优化、设备适配、以及性能监测的职责。目前对于页面的监控通常会通过W3C性能小组引入的新API来获取页面的性能,目前IE9以上的浏览器都支持,在window.performance中里面存在多种参数,我们可以自由组合来达到我们想要监控的效果。
监控什么
google开发者提出一种rail模型来衡量应用性能,即response、animation、idle、load。分为代表web应用生命周期的四个不同方面。并指出最好恶毒性能指标是:100ms内响应用户输入,动画或者滚动需要在10ms内产生下一帧,最大化空闲时间,页面加载时长不超过5s。
我们结合上面的要求可做的方面有:响应速度(页面初始访问速度、交互响应速度)、页面稳定性(页面出错率)、外部服务调用(网络请求访问速度)
- 页面访问速度:白屏、首屏时间、可交互时间
- 首次渲染、首次有内容的渲染
- 首次有意义的渲染、页面关键元素:如何定义有意义的渲染?通常假设页面dom结构发生剧烈变化,或者页面内主要内容出现的时候,这个时候我们可以称之为有意义的渲染,这个标准不是统一的
- 可交互时间
- 常任务:浏览器是单线程的,如果常任务过多,回影响用户响应时长
- 页面稳定性:页面出错情况
- 资源加载错误
- js执行错误
- 外部服务调用
- CGI耗时
- CGI成功率
- CDN资源耗时
performance的结构
当我们在控制台中输入window.performance。会看到里面输出了performance的结构,包括:memory、navigation、onresourcetimingbuggerfull、timeOrigin、timing五个属性值。
1. memory 内容相关
performance.memory显示此刻内容的占用情况,它是一个动态值。
名称 | 意义 |
---|---|
usedJsHeapSizeLimit | 占用的内存数 |
totalJsHeapSize | 可使用的内存 |
jsHeapSizeLimit | 内存大小限制 |
通常当usedJsHeapSizeLimit大于totalJsHeapSize时,有可能出现了内存泄露
2、navigation 来源相关
在navigation中存在2个属性值,redurectCount表示重定向的次数,默认是0。type表示页面的打开方式
类型 | 意义 |
---|---|
0 | 正常进入页面 |
1 | 刷新的页面 |
2 | 浏览器的前进后退进入的页面 |
255 | 非以上的方式 |
3、onresourcetimingbufferfull 缓冲区满后回调函数
该属性是一个在resourcetimingbufferfull事件触发时会被调用的事件函数,它的值时一个手动设置的回调函数,这个回调函数会在浏览器的资源时间性能缓冲区满时执行
4、timeOrigin 一系列时间的基准点
5、timeing一系列关键时间点,包含了网络、解析等一系列的时间数据
针对以上时间节点的解释
通过上面这些参数,我们可以获取页面的Domready时间、onload时间、白屏时间等,以及单个页面资源在发送请求到获取到response各阶段的性能参数。比较常用的性能数据计算方法如下:
首次渲染、首次有内容的渲染
目前这个指标已经标准化,可通过performance.getEntriesByType('paint')查看。首次渲染和首次有内容渲染这两个时间,通常情况下是相同的
白屏时间:responseStart - navigationStart
浏览器从服务端接收第一个字节时间 - 同一个浏览器上一个页面卸载结束时间,如果没有上一页,则同fetchstart,即浏览器准备好使用http请求文档的时间或查询缓存之前
首页可交互时间:domInteractive - fetchStart
粗略首屏时间: loadEventEnd - fetchStart 或者 domInteractive - fetchStart
重定向耗时: redirectEnd - redirectStart
- 重定向完成时间 - 重定向开始时间
SSL安全链接耗时:connectEnd - secureConnectionStart
DNS查询耗时:domainLookupEnd - domainLookupStart
DNS查询完成时间 - DNS查询开始时间,如果DNS解析读取的是缓存或者本地资源,这两个值都和fetchstart一致,即DNS耗时为0
TCP链接耗时:connectEnd - connectStart
域名查询结束的时间 - 浏览器与服务器之间建立连接的时间。
HTTP请求耗时:responseEnd - responseStart
浏览器从服务器接受最后一个字节时间 - 浏览器向服务器发出http请求的时间
解析dom树耗时:domComplete - domInteractive
文档解析完成时间 - dom解析结束,开始加载资源时候
即document.readyState变为complete且对应的readyStatechange被触发的时间 - document.readystate变为interactive。相应的readyStatechange事件触发
页面完全加载时间: loadEventStart - fetchStart
http 头部大小: transferSize - encodedBodySize
DOMready时间:domContentLoadedEventEnd - navigationStart
当所有需要需要立即执行的脚本已经被执行时间 - 上一个页面被浏览器卸载结束时间或者发送ajax请求的开始时间
onload时间:loadEventEnd - navigationStart即是onload回调函数执行时间
性能优化
- 重定向优化: 重定向的类型分为三种,301(永久重定向)、302(临时重定向)、304(not Modified)。304是用来处理缓存,不用处理。前两张应该尽可能的避免,如果有需要重定向的代码,可以把重定向后的地址写到html或js中,可以减少客户端和服务端的通信过程,节省重定向耗时。
- DNS优化:
一般在前端优化中与DNS有关的就2点,一个是减少DNS的请求次数,另一个就是DNS预获取。减少DNS解析次数的比较好的方式就是尽量把资源放在一个cdn域名上。DNS与解析是让具有此属性的玉米不需要用户点击链接就在后台解析,而郁闷解析和内容载入是传销的网络操作,所以这个操作可以减少用户的等待时间,提升用户体验。新版浏览器会对不同域名进行预获取并缓存结果,进行隐式DNS预解析。在页面中主动进行预解析可以在html的head中植入
<link rel="dns-prefetch" href="//baidu.com" />
- TCP请求优化 前端能做到TCP优化就是减少请求数量,http1.0默认使用短连接,也就是客户端和服务端每进行一次http操作就需要建立一个连接,包括三次握手,四次挥手,传输完成即中断连接,极大的消耗了时间。在使用http1.1时,可以在http的响应头上增加connection:keep-alive,当一个网页打开完成之后,连接不会中断,当再次访问这个服务的时候,会继续使用这个连接。或者使用websocket进行通信,全程需要一次TCP链接
- 渲染优化 在浏览器段的渲染过程,如大型框架vue、react,它的模版其实是在浏览器端执行框架代码进行渲染的。这个过程对于首屏的展现,白屏的持续时间会有所增加。为了更快的向页面展示效果,我们可在服务端就进行整个html渲染,然后将渲染后的html直出到浏览器端。客户端渲染的时候,通过script标签引入的js文件也会阻止解析器解析dom。针对这种情况,我们可以选择把script外链放在页面底部,也可以使用async、defer属性来延迟执行。defer是有序的,会按照标签顺序执行,而async是无序的,文件只要加载完就会立即执行。除此还可以通过定时器等方案动态生成script插入dom中。
页面资源的性能统计
在performance.timing中记录着页面整体性能的指标参数,如果要获取加载的js、图片等资源的性能指标,就需要使用Resource Timing API。performance.getEntries()方法,包含了所有静态资源的数组列表,每一项是一个请求的相关参数有name、type、时间等。当我们在控制台通过performance.getEntries()查看PerformanceResourceTiming数据时,可以看到与通用的performance.timing对比,没有与dom相关的属性,新增了资源name、entryType、initiatorType和duration四个属性。
- name表示: 资源名称,也是资源的绝对路径,可通过performance.getEntriesByName来获取这个资源的具体属性
- entryType表示:资源类型“resource”,此外还有navigation、mark、measure这三种
- intiatorType表示:请求来源:link、script等
获取页面资源的情况
通过performance.getEntriesByType('resource')获取entryType为resource的资源数据
结合
可以计算出资源的加载时间,可测量图片、js、css、或者XHR的性能数据
// 某类资源的加载时间,可测量图片、js、css、XHR
const resourceListEntries = performance.getEntriesByType("resource");
resourceListEntries.forEach(resource => {
//let p = window.performance.getEntries();
// JS 资源数量:p.filter(ele => ele.initiatorType === "script").length
// CSS 资源数量:p.filter(ele => ele.initiatorType === "css").length
// AJAX 请求数量:p.filter(ele => ele.initiatorType === "xmlhttprequest").length
// IMG 资源数量:p.filter(ele => ele.initiatorType === "img").length
// 总资源数量: window.performance.getEntriesByType("resource").length
if (resource.initiatorType == 'img') {
console.info(`Time taken to load ${resource.name}: `,
resource.responseEnd - resource.startTime);
}
});
// 页面js总加载耗时
const p = window.performance.getEntries();
let cssR = p.filter(ele => ele.initiatorType === "script");
Math.max(...cssR.map((ele) => ele.responseEnd)) - Math.min(...cssR.map((ele) => ele.startTime));
// css总加载耗时
const p = window.performance.getEntries();
let cssR = p.filter(ele => ele.initiatorType === "css");
Math.max(...cssR.map((ele) => ele.responseEnd)) - Math.min(...cssR.map((ele) => ele.startTime));
统计某个函数的耗时
主要是利用 mark 和 measure 方法去打点计算某个阶段的耗时,例如某个函数的耗时等
async function run() {
performance.mark("startTask1");
await doTask1(); // Some developer code
performance.mark("endTask1");
performance.mark("startTask2");
await doTask2(); // Some developer code
performance.mark("endTask2");
// Log them out
const entries = performance.getEntriesByType("mark");
for (const entry of entries) {
console.table(entry.toJSON());
}
}
run();
首屏时间的计算
首屏计算的方式有哪些?
- 用户自定义打点
- lighthouse插件中,使用chrome渲染过程中记录的trace event
- 拿到页面布局的节点数目。获取当页面具有最大布局变化的时间点
- 利用MutationObserver接口,监听document对象的节点变化。检测这些节点是否在首屏中,如果在首屏中,那当前的时间点为首屏的渲染时间。但是还有首屏内的图片的加载时间,通过上面的获取所有图片实体对象,根据图片的初始加载时间和加载完成时间去更新首屏渲染时间
- 在首屏的内容模块中插入一个指定的div,利用MutationObserver的接口监听该dom的高度变化,如果大于指定值,说明有内容渲染出来了,可据次计算首屏时间
- 在loading状态下循环判断当前页面高度是否大雨屏幕高度,若大于,则获取当前页面的屏幕图像,通过逐像素对比来判断页面渲染是否已满屏
基本性能上报
在上报的过程中我们要考虑的因素有很多,比如数据的准确、不影响性能、数据的完整度等。我们知道通常在页面中如果关闭浏览器,那么发送的数据会被终止发送,这样的话数据就会缺失。google开发者推荐的上报方式
异常上报
异常有:
- js error异常: 监听window.onerror事件
- promise reject的异常: unhandledrejection事件
window.addEventListener("unhandledrejection", function (event) {
console.warn("WARNING: Unhandled promise rejection. Shame on you! Reason: " + event.reason);
});
- 资源加载失败: window.addEventListener('error')
- 网络请求失败: 重写 window.XMLHttpRequest 和 window.fetch 捕获请求错误
- iframe 异常: window.frames[0].onerror
- window.console.error
CGI上报
大致原理:拦截 ajax 请求,数据存储与聚合. 一个用户访问,可能会上报几十条数据,每条数据都是多维度的。即:当前访问时间、平台、网络、ip 等。这些一条条的数据都会被存储到数据库中,然后通过数据分析与聚合,提炼出有意义的数据。例如:某日所有用户的平均访问时长、pv 等。
受同源策略影响,跨域资源获取到的时间点通常为0,如果需要更详细准确的时间点,可以翻去请求资源通过performance.getEntriesByName获取。或者资源服务器开启响应头Timing-Allow-Origin,添加指定来源站点。比如Timing-Allow-Origin: https//baidu.com
资源缓冲区监控
setResourceTimingBufferSize设置当前页面可缓存的最大资源数据个数,超出时会清空所有entryTyoe为resource的资源数据。通过onresourcetimingbufferfull事件监控资源缓冲区的情况,如果超过设置的资源个数会触发此事件。通过clearResourceTimings方法移除浏览器性能数据缓冲区中所有的entryType时resource的资源
performance方法说明
- getEntries(): 获取所有静态资源的数组列表
- now():当前页面执行的时间,与Date.now()不同,它是毫秒为单位,不会系统程序执行阻塞的影响,比Date.now()更精准。performance.timing.navigationStart + performance.now()约等于Date.now()。如果我们要测试一段代码执行了多少时间,可以通过peroformance.now()来进行打点会比Date.now()更加精准
- mark()用来标记时间
- measure() 在浏览器指定的start mark和end mark间的性能输入缓冲区中创建一个指定名称的时间戳。
- gerEntriesByname()和getEntriesByType():返回performanceEntry对象的列表,两个函数给的参数不同。
示范如图,通过mark标记一个markstart和markend的开始和结束值,通过measure在缓冲区缓存起来。然后可以通过performance.getEntriesByname( ‘myMeasure’ )或者performance.getEntriesByType(‘measure’)查询。
总结
通过这些属性和方法,可以准确的记录下我们想要的时间,结合特定的逻辑处理和方法,我们可以很容易掌握我们网站的各项性能指标。目前主流浏览器都已支持performance对象,但是不支持全部的属性和方法。