前端监控系列-性能监控
字数 4k / 14 min 读完 / 阅读什么是性能监控,主要要监控哪些指标,有什么意义呢?
我们经常会遇到这样一个情况,发布在线上的页面,有一天老板或者是其他同学跟你反馈,你的页面怎么这么慢,是不是有bug。然后你会自己打开对应的页面,看了看感觉速度正常,也算是秒开吧。然后你再关闭缓存,再试了一下,还是好的呀,哪里慢了?这个时候你就很难评估出来这个页面到底是真慢还是某个设备的网络问题。
这种在单个设备上的表现,其实很难准确反映我们的页面在线上运行的真实情况。线上环境包括不同的设备、不同的宿主环境、不同的运营商网络、不同的地区这些关联因素,所以我们就需要有一套可以监控线上环境运行情况的系统。
性能监控指标
那监控线上环境,我们需要监控哪些指标呢?哪些指标能准确的反映出前端页面的运行性能呢?
chrome上的performance指标
- first paint (FP):这个指标标志着浏览器渲染第一个像素点的时间。
- first contentful paint (FCP):和FP标志着任意一个像素点被渲染的时间不同,FCP 标志着浏览器渲染第一个内容元素的时间,这些内容元素可以是text、image、svg、canvas。
- First meaningful paint (FMP):这个指标标志着首屏最重要的一块区域的渲染,通常是用户最关注的区域。比如视频网站的视频播放区域,搜索网站的第一个搜索结果区域,又或者是购物网站的照片首图。(Lighthouse 6.0 中已不推荐使用 FMP)
- Largest Contentful Paint(LCP):由 Web 孵化器社区组(WICG)在 Largest Contentful Paint API 中提出,是一个非标准化的 Web 性能度量。可视区域中最大的内容元素呈现到屏幕上的时间,用以估算页面的主要内容对用户可见时间。
- First Screen Paint(FSP):由百度在 W3C 标准提案 First Screen Paint 中提出。页面从开始加载到首屏内容全部绘制完成的时间,用户可以看到首屏的全部内容。
- Onload Event(L): 代表整个HTML 渲染完成,包括所有资源、样式都已经在家完成
- Time to interactive (TTI):由 Web 孵化器社区组(WICG)在 Time To Interactive 中提出,是一个非标准化的性能度量指标。这个指标表示浏览器已经渲染完了我们首屏需要显示的内容并且已经准备好接受用户的交互信息了,也标志着程序是否可用。
- First CPU Idle(FCI):由网络孵化器社区小组提出的 First Interactive 指标,并已被用于各种工具中。这个指标在 LightHouse 中称为 First CPU Idle(FCI)。页面第一次可以响应用户输入的时间。FCI 和 TTI 都是页面可以响应用户输入的时间。FCI 发生在用户可以开始与页面交互时;TTI 发生在 用户完全能够(可持续) 与页面交互时。
所以根据上面这些性能指标,我们大概可以确定出来几个对应的评估标准。
用户体验核心指标 | 定义 | 衡量指标 |
---|---|---|
白屏时间 | 页面开始有内容的时间,在没有内容之前是白屏 | FP 或 FCP |
首屏时间 | 可视区域内容已完全呈现的时间 | FSP |
可交互时间 | 用户第一次可以与页面交互的时间 | FCI |
可流畅交互时间 | 用户第一次可以持续与页面交互的时间 | TTI |
性能指标API
关于前端性能指标,W3C 定义了强大的 Performance API,其中又包括了 High Resolution Time 、 Frame Timing 、 Navigation Timing 、 Performance Timeline 、Resource Timing 、 User Timing 等诸多具体标准。Web 性能标准则是在 window 上添加了 performance 属性,通过 window.performance 返回一个 Performance 对象。
对象中包含了很多用于衡量性能的属性和方法,而这些属性和方法由多种 Web 性能标准定义。 详细介绍可以查看这篇文章
不过上面的对象在不同浏览器上也有不同,下面是safari的数据
performance.navigation
- redirectCount
如果有重定向的话,页面通过几次重定向跳转而来 - type
- 0 即 TYPE_NAVIGATENEXT 正常进入的页面(非刷新、非重定向等)
- 1 即 TYPE_RELOAD 通过 window.location.reload() 刷新的页面
- 2 即 TYPE_BACK_FORWARD 通过浏览器的前进后退按钮进入的页面(历史记录)
- 255 即 TYPE_UNDEFINED 非以上方式进入的页面
navigation字段统计的是一些网页导航相关的数据:
- redirectCount:重定向的数量(只读),但是这个接口有同源策略限制,即仅能检测同源的重定向;
- type 返回值应该是0,1,2 中的一个。分别对应三个枚举值:
- 0 : TYPE_NAVIGATE (用户通过常规导航方式访问页面,比如点一个链接,或者一般的get方式)
- 1 : TYPE_RELOAD (用户通过刷新,包括JS调用刷新接口等方式访问页面)
- 2 : TYPE_BACK_FORWARD (用户通过后退按钮访问本页面)
performance.timing
下面时间对照图对应上面performance.timing对象
名称 | 注释 |
---|---|
navigationStart | 浏览器窗口的前一个网页关闭时发生unload事件时的Unix时间戳,属于最前的测量时间点 |
unloadEventStart | 前网页与当前网页同属一个域名时,返回前一个网页的unload事件发生时的Unix时间戳 |
unloadEventEnd | 前网页与当前网页同属一个域名时,返回前一个网页unload事件的回调函数结束时的Unix时间戳 |
redirectStart | 返回第一个HTTP跳转开始时的Unix时间戳 |
redirectEnd | 返回最后一个HTTP跳转结束时的Unix时间戳 |
fetchStart | 返回浏览器准备使用HTTP请求读取文档等资源时的Unix时间戳,在网页查询本地缓存之前发生 |
domainLookupStart | 返回域名查询开始时的Unix时间戳。如果使用持久连接,或者信息是从本地缓存获取的,则返回值等同于fetchStart属性的值 |
domainLookupEnd | 返回域名查询结束时的Unix毫秒时间戳。如果使用持久连接,或者信息是从本地缓存获取的,则返回值等同于fetchStart属性的值 |
connectStart | 返回HTTP请求开始向服务器发送时的Unix毫秒时间戳。如果使用持久连接(persistent connection),则返回值等同于fetchStart属性的值 |
connectEnd | 返回浏览器与服务器之间的连接建立时的Unix毫秒时间戳。如果建立的是持久连接,则返回值等同于fetchStart属性的值。连接建立指的是所有握手和认证过程全部结束 |
secureConnectionStart | 返回浏览器与服务器开始安全链接的握手时的Unix毫秒时间戳。如果当前网页不要求安全连接,则返回0 |
requestStart | 返回浏览器向服务器发出HTTP请求时(或开始读取本地缓存时)的Unix毫秒时间戳 |
responseStart | 返回浏览器从服务器收到(或从本地缓存读取)第一个字节时的Unix毫秒时间戳 |
responseEnd | 返回浏览器从服务器收到(或从本地缓存读取)最后一个字节时(如果在此之前HTTP连接已经关闭,则返回关闭时)的Unix毫秒时间戳 |
domLoading | 返回当前网页DOM结构开始解析时(即Document.readyState属性变为“loading”、相应的readystatechange事件触发时)的Unix毫秒时间戳 |
domInteractive | 返回当前网页DOM结构结束解析、开始加载内嵌资源时(即Document.readyState属性变为“interactive”、相应的readystatechange事件触发时)的Unix毫秒时间戳 |
domContentLoadedEventStart | 返回当前网页DOMContentLoaded事件发生时(即DOM结构解析完毕、所有脚本开始运行时)的Unix毫秒时间戳 |
domContentLoadedEventEnd | 返回当前网页所有需要执行的脚本执行完成时的Unix毫秒时间戳 |
domComplete | 返回当前网页DOM结构生成时(即Document.readyState属性变为“complete”,以及相应的readystatechange事件发生时)的Unix毫秒时间戳 |
loadEventStart | 返回当前网页load事件的回调函数开始时的Unix毫秒时间戳。如果该事件还没有发生,返回0 |
loadEventEnd | 返回当前网页load事件的回调函数运行结束时的Unix毫秒时间戳。如果该事件还没有发生,返回0 |
核心指标计算
白屏时间
白屏时间代表的是,用户从点击链接打开页面开始,到页面上有内容展示的时间长度。这个过程包括dns查询、建立tcp连接、发送首个http请求(如果使用https还要介入TLS的验证时间)、返回html文档、html文档head解析完毕。
domLoading 代表浏览器开始解析html文档的时间节点。我们知道IE浏览器下的document有readyState属性,domLoading的值就等于readyState改变为loading的时间节点;
domInteractive 为在主文档的解析器结束工作,即 Document.readyState 改变为 ‘interactive’ 并且相当于 readystatechange 事件被触发之时的 Unix毫秒时间戳。
// 在不支持getEntriesByType()的情况下,使用timing
var whiteScreenTime = performance.timing.domInteractive - performance.timing.navigationStart
// 支持getEntriesByType(),没有domLoading时机,所以实用domInteractive
if (performance.getEntriesByType && performance.getEntriesByType('paint').length) {
const paintList = performance.getEntriesByType('paint')
paintList.map((item) => {
if (item.name === 'first-contentful-paint') {
whiteScreenTime = item.startTime
}
})
}
首屏时间
首屏时间的统计目前没有标准的方法,而且每个不同类型的页面都有不同的评判标准,所以较为复杂,而且会涉及图片、iframe 等多种元素的异步加载和渲染。但是为了这个方案更加通用、方便,所以我们以下面两个原则做为基本原则导向。
- 首屏时间计算模块不耦合业务,作为一个单独运行的 js 脚本文件进行单独引用,尽量不对外暴露 API 接口给开发者使用,所有采集任务都有模块内部实现完成。
- 性能和准确性的权衡,业界通过指定时间间隔,进行 canvas 截屏(即 canvas2Html),计算像素点的前后像素点的的差别。准确性是很高,但是在性能方面就差强人意了。
并结合我们现有业务,发现其实最大的影响因素还是在加载图片上面,所以计算首屏的规则大致可以分成两种情况:
- 首屏有图片,计算在首屏的所有图片,全部加载完成的时间
let time = 首屏图片加载完成时间 - performance.timing.navigationStart; // 浏览器地址栏输入url回车之后,或用户点击打开href的时间
- 首屏没有图片,使用dom加载完成时间
let time = 页面处于稳定状态前最后一次 dom 变化的时刻 - window.performance.timing.navigationStart
图片加载时间
- 从进入页面开始,每隔500ms,对 html 上的 dom 元素进行遍历8次,保证再接口返回后,找到在首屏展示的图片,并绑定 onload 和 onerror 事件
- (1. 绑定时做判断,不要重复绑定,2.绑定时判断图片是否相同,相同不需要重复绑定)
- 统计图片 onload 事件的图片完成加载时间戳,计算对比出最后加载完成的时间
- 计算出首屏加载时间
具体的实现流程如下:
后续
开发过程中的问题
- performance API 浏览器支持程度不同(已解决)
- 需要获取background-image的图片加载时间(已解决)
- background-image 如何监听加载时间,是否准确
- background-image 获取定位位置,判断是否为首屏
- 在首屏但是不在可视范围内的图片,如swiper(已解决)
- dom 加载时机不同,onload 之后接口返回数据 dom 变化(已解决)
- 多次循环遍历所有图片资源,限制最终节点
- 为什么 allLoadedTime 时间会比 fistPageLoadTime 时间还要长(已解决)
- 因为会在代码开始后的 2s 中内循环去查到 dom 节点,所有在接口还未请求回来之前,loaded 时间就已经触发了,所以 firstPageLoadTime 会比 loaded 时间还长
- 监听事件重复添加(已解决)
优化点
与客户端结合,可以监控上传一些客户端信息
// 需要客户端提供的字段 nativeLoadData: { webviewInit // webview 初始化的时机 isCache // 是否存在缓存 orginalUrl // 原始链接 completeUrl // 最终链接 } // 触发时机,是否可以是有值就塞进去
上传时机有问题,选择更好的时机上报
上报方式有问题,结合 sendbeacon 方式,需要向下兼容
查找 dom 算法可以优化
需要考虑内嵌iframe的情况
完全加载时间
var onLoadTime = performance.timing.loadEventEnd - performance.timing.navigationStart // onload时间
// 完全加载时间
var fullScreenTime = 页面有图片 ? 页面内所有图片加载完成的时间 - performance.timing.navigationStart : onLoadTime
页面内所有图片加载完成的时间有两种方式可以获得:
- 通过上面循环获取图片,监听最后一张图片加载完的时间
- 通过
performance.getEntriesByType('resource')
方式获取资源加载时间,筛选出最后一张图片加载完成的时间
if (performance.getEntriesByType && performance.getEntriesByType('navigation').length && performance.getEntriesByType('resource').length) {
const timging2 = performance.getEntriesByType('navigation')[0] // 返回的是相对时间
const resourceList = performance.getEntriesByType('resource')
let lastItem = {}
resourceList.map((item) => {
// 判断onload 之前发起的图片请求,排除用户交互之后的图片加载
if (item.initiatorType === 'img' && item.startTime < timging2.loadEventEnd) { // 可以增加特定的筛选逻辑
if (item.responseEnd > lastItem.responseEnd) {
lastItem = JSON.parse(JSON.stringify(item))
}
}
})
obj.fullScreenTime = lastItem.responseEnd >= timging2.loadEventEnd ? lastItem.responseEnd : timging2.loadEventEnd
}
其他时间
var redirectTime = performance.timing.redirectEnd - performance.timing.redirectStart
// 重定向时间
var dnsTime = performance.timing.domainLookupEnd - performance.timing.domainLookupStart
// DNS解析时间
var tcpTime = performance.timing.connectEnd - performance.timing.connectStart
// TCP完成握手时间
var ajaxTime = performance.timing.responseEnd - performance.timing.requestStart
// HTTP请求响应完成时间
var domTime = performance.timing.domComplete - performance.timing.domLoading
// DOM加载完成时间
var whiteScreenTime = performance.timing.domLoading - performance.timing.navigationStart
// DOM开始加载前所花费时间
var ttfbTime = performance.timing.responseStart - performance.timing.navigationStart
// TTFB 读取页面第一个字节的时间
关于其他一些时间的计算,可以查看这篇文章
思考问题
SPA
单页面应用要怎么办?怎么去监听每个页面变化后的加载时间呢?
window.performance
API 是有缺点的,在 SPA 切换路由时,window.performance.timing
的数据不会更新。所以我们需要另想办法来统计切换路由到加载完成的时间。拿 Vue 举例,一个可行的办法就是切换路由时,在路由的全局前置守卫 beforeEach
里获取开始时间,在组件的 mounted
钩子里执行 vm.$nextTick
函数来获取组件的渲染完毕时间。
router.beforeEach((to, from, next) => {
store.commit('setPageLoadedStartTime', new Date())
})
mounted() {
this.$nextTick(() => {
this.$store.commit('setPageLoadedTime', new Date() - this.$store.state.pageLoadedStartTime)
})
}
除了性能和错误监控,其实我们还可以收集更多的信息。