前端性能监控在优化用户体验和提升业务指标方面发挥着关键作用(主要是可以拿着性能数据对外吹牛B)。有需求就有产品,目前主流云厂商基本提供了类似的SaaS产品,比如阿里云的ARMS 用户体验监控,腾讯云的前端性能监控 RUM。
这些SaaS产品提供了非常丰富的性能指标,然而这些指标对于小规模应用真的有必要吗?项目真的有预算去买这些服务?答案是否定的,在项目规模没起来前,谁管那么细的性能指标啊,能关注个首屏时间就很不错了。
好的,作为开发者我们肯定想了解一下自己web应用的性能指标,怎么做呢?请看下文,包你10分钟快速实现主要性能指标的采集与上报。
核心指标
关于性能指标有很多,目前社区必要认可的监控指标体系是 GoogleChrome 提出的web-vitals关键指标集合:
- CLS:整个页面生命周期内发生的所有意外布局偏移中最大一连串的布局偏移分数
- FID:用户第一次和页面交互后(比如点击按钮),浏览器响应并处理事件的时间
- LCP:最大内容绘制的时间,即视口内最大元素渲染的时间点
- FCP:首次内容绘制,开始加载到页面内容的任何部分在屏幕上完成。
- TTFB:页面返回首字节时间,用来判断页面资源返回速度
备注:在最新的 web-vital指标体系里 FID 已经被 INP 所取代,并且官方明确在下一个 release版本中将移除 FID指标
考虑到认可度,本文还是采纳老的指标体系
性能指标采集
明确了性能指标,那就开工吧。
首先要明白对于性能指标,最可靠,最稳妥的获取方式一定是通过浏览器提供的"原生" BOM 接口实现,带着这个假设去 MDN 上搜索相关草案,还真的就找到了一个 API
PerformanceObserver()
PerformanceObserver()
构造函数使用给定的观察者callback
生成一个新的PerformanceObserver
对象。当通过observe()
方法注册的 条目类型 的 性能条目事件 被记录下来时,调用该观察者回调。语法
var observer = new PerformanceObserver(callback);
通过提问 GPT,可以得知 PerformanceObserver
源自 2022 年 11 月 15 日发布了候选推荐草案里的Performance Timeline Level 2
标准。
额~,2022年提出的草案,也就说是 2022 年之前版本的浏览器就不要想着能完全使用了。不过没关系,我们还有兜底方案,首先根据 MDN 指引文档来借助 PerformanceObserver
实现上述 5 个核心指标的采集
PerformanceObserver
此处略过为什么,因为我也不知道,按照 MDN 文档提供的API使用就是了
具体实现如下:
js
// CLS
const clsObserver = new PerformanceObserver(entryList => {
const clsEntries = entryList.getEntries()
const clsValue = clsEntries.reduce((sum, entry: any) => sum + entry.value, 0)
console.log('CLS:', clsValue)
})
clsObserver.observe({ type: 'layout-shift', buffered: true })
// FID
const fidObserver = new PerformanceObserver(entryList => {
const fidEntry: any = entryList.getEntries()[0]
const fidValue = fidEntry.processingStart - fidEntry.startTime
console.log('FID:', fidValue)
})
fidObserver.observe({ type: 'first-input', buffered: true })
// LCP
const lcpObserver = new PerformanceObserver(entryList => {
const lcpEntry = entryList.getEntries()[0]
const lcpValue = lcpEntry.startTime
console.log('LCP:', lcpValue)
})
lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true })
// FCP
const fcpObserver = new PerformanceObserver(entryList => {
const fcpEntry = entryList.getEntries()[0]
const fcpValue = fcpEntry.startTime
console.log('FCP:', fcpValue)
})
fcpObserver.observe({ type: 'paint', buffered: true })
// TTFB
const ttfbObserver = new PerformanceObserver(entryList => {
const clsEntries = entryList.getEntries()
const clsValue = clsEntries.reduce((sum, entry: any) => sum + entry.value, 0)
console.log('TTFB:', clsValue)
})
ttfbObserver.observe({ type: 'navigation', buffered: true })
唉,上面的代码怎么只有采集没有上报?...数据都采集到了,写个 Restful 接口实现上报应该不难了吧^_^。
好的,从上面代码可以看到采集的关键是 observe
方法,而具体是哪个指标则是由 type: xxx
来决定的。刚才我们也提到了兼容性问题,那么 PerformanceObserver
的兼容性如何呢?
可以看到 PerformanceObserver
本身的兼容性还是不错的,但是,其对指标采集起到重要作用的 type
兼容性就呵呵了,以 LCP
为例:
在 Safari
下完全不支持,在国产浏览器上的支持度不明。
那怎么办?既然GoogleChrome提出了这些核心指标集,ta肯定也提供了上报手段,那就是web-vitals
The
web-vitals
library is a tiny (~2K, brotli'd), modular library for measuring all the Web Vitals metrics on real users, in a way that accurately matches how they're measured by Chrome and reported to other Google tools (e.g. Chrome User Experience Report, Page Speed Insights, Search Console's Speed Report).The library supports all of the Core Web Vitals as well as a number of other metrics that are useful in diagnosing real-user performance issues.
所以我们可以用 web-vitals
这个库来做兜底,当浏览器不支持PerformanceObserver
相应的 type
时,调用web-vitals
暴露的方法实现性能指标采集。
web-vitals
web-vitals
的使用非常简单:
js
import { onCLS, onFID, onFCP, onLCP, onTTFB } from "web-vitals";
const reportWebVitals = () => {
onCLS((metric) => {
console.log("CLS = ", metric);
});
onFCP((metric) => {
console.log("FCP = ", metric);
});
onFID((metric) => {
console.log("FID = ", metric);
});
onLCP((metric) => {
console.log("LCP = ", metric);
});
onTTFB((metric) => {
console.log("TTFB = ", metric);
});
};
export default reportWebVitals;
是不是很简单,直接在暴露出的相关方法的回调里接收数值就好了。
最终版
综合上面的措施,在PerformanceObserver
监听的基础上加一点兼容代码,就可以得到相对完善的性能监控方法:
js
import { onLCP, onFID, onCLS, onFCP, onTTFB } from 'web-vitals'
// CLS
function observeCLS() {
if (this.supportedEntryTypes.includes('layout-shift')) {
const clsObserver = new PerformanceObserver(entryList => {
const clsEntries = entryList.getEntries()
const clsValue = clsEntries.reduce((sum, entry: any) => sum + entry.value, 0)
console.log('CLS:', clsValue)
})
clsObserver.observe({ type: 'layout-shift', buffered: true })
} else {
onCLS(res => {
this.WebPerformanceTracer.startActiveSpan('cls', clsSpan => {
clsSpan.setAttributes({ cls: res.value, path: location.pathname })
clsSpan.end()
})
})
}
}
// FID
function observeFID() {
if (this.supportedEntryTypes.includes('first-input')) {
const fidObserver = new PerformanceObserver(entryList => {
const fidEntry: any = entryList.getEntries()[0]
const fidValue = fidEntry.processingStart - fidEntry.startTime
console.log('FID:', fidValue)
})
fidObserver.observe({ type: 'first-input', buffered: true })
} else {
onFID(res => {
console.log('FID:', res.value)
})
}
}
// LCP
function observeLCP() {
if (this.supportedEntryTypes.includes('largest-contentful-paint')) {
const lcpObserver = new PerformanceObserver(entryList => {
const lcpEntry = entryList.getEntries()[0]
const lcpValue = lcpEntry.startTime
console.log('LCP:', lcpValue)
})
lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true })
} else {
onLCP(res => {
console.log('LCP:', res.value)
})
}
}
// FCP
function observeFCP() {
if (this.supportedEntryTypes.includes('paint')) {
const fcpObserver = new PerformanceObserver(entryList => {
const fcpEntry = entryList.getEntries()[0]
const fcpValue = fcpEntry.startTime
console.log('FCP:', fcpValue)
})
fcpObserver.observe({ type: 'paint', buffered: true })
} else {
onFCP(res => {
console.log('FCP:', res.value)
})
}
}
// TTFB
function observeTTFB() {
if (this.supportedEntryTypes.includes('navigation')) {
const ttfbObserver = new PerformanceObserver(entryList => {
const clsEntries = entryList.getEntries()
const clsValue = clsEntries.reduce((sum, entry: any) => sum + entry.value, 0)
console.log('TTFB:', clsValue)
})
ttfbObserver.observe({ type: 'navigation', buffered: true })
} else {
onTTFB(res => {
console.log('TTFB:', res.value)
})
}
}
怎么样?是不是很简单就实现了web站点的核心性能指标采集。
总结
- 一般来讲在项目没有起规模前,只需要关注web应用的核心指标就行;
- 通过
PerformanceObserver
可以实现初步的性能指标采集能力; - 借助
web-vitals
库可以进一步提供性能指标采集的适用性、兼容性。