前言
在前端开发中,性能优化总是绕不开的话题。我们都知道,一个交互流畅的网页能够大大提升用户体验,留住更多用户。而要实现这一目标,精准地监控和分析网页性能就显得尤为重要。提到性能监控,你可能会想到performance API,但今天,我想带你一起探索一个更加灵活、强大的工具——PerformanceObserver。
PerformanceObserver并不是要完全取代performance API,而是提供了一个更加主动、事件驱动的方式来监听和处理性能数据。想象一下,你不再需要手动去查询或轮询性能数据,而是可以设置一个观察者,让它在你关心的性能事件发生时自动通知你。
我们可以通过 PerformanceObserver 监测如下数据,也许你已经使用过这些性能指标,并且想深入了解下如何获取这些指标可以跟着文章一探究竟!
为什么使用 PerformanceObserver
那么,为什么我们要选择PerformanceObserver呢?它的独特优势在于:
- 实时性:PerformanceObserver能够实时捕获性能事件,让你在第一时间了解网页的性能表现。这对于及时发现并解决问题至关重要。
- 灵活性:通过配置PerformanceObserver的回调函数,你可以自定义处理性能数据的方式。无论是简单的日志记录,还是复杂的性能分析,都能轻松应对。
- 可扩展性:随着Web标准的不断发展,PerformanceObserver支持的性能条目也在不断增加。这意味着你可以用它来监控更多类型的性能数据,满足日益增长的性能优化需求。
- 易用性:虽然PerformanceObserver提供了强大的功能,但它的API设计相对简洁直观。即使是初学者,也能较快上手并应用到实际项目中。
PerformanceObserver
基础示例
下面我将通过一个简单的例子来介绍PerformanceObserver的基础使用。这个例子将展示如何设置一个PerformanceObserver来监听页面上的resource性能条目,即资源加载事件。
// 创建一个 PerformanceObserver 实例
const performanceObserver = new PerformanceObserver((list) => {
// 回调函数会在每次有匹配的 PerformanceEntry 被添加到 PerformanceTimeline 时被调用
for (const entry of list.getEntries()) {
// 检查 entry 类型是否为我们关注的 'resource'
if (entry.entryType === 'resource') {
console.log(`Resource loaded: ${entry.name}`);
console.log(`Duration: ${entry.duration} ms`);
console.log(`Initiator Type: ${entry.initiatorType}`); // 哪个类型的事件触发了这个资源的加载
// 可以根据需要添加更多日志或处理逻辑
}
}
});
// 告诉 PerformanceObserver 我们想要监听哪些类型的 PerformanceEntry
// 在这个例子中,我们监听 'resource' 类型的条目
performanceObserver.observe({ type: 'resource' });
静态方法
通过上面的基础示例了解了 PerformanceObserver 的使用, 除了监听 resource 还可以监听其他的类型。你可以通过PerformanceObserver 的静态方法 supportedEntryTypes 查询当前浏览器支持哪些类型的性能条目(PerformanceEntry)。
// 检查浏览器支持的 PerformanceEntry 类型
const supportedTypes = PerformanceObserver.supportedEntryTypes;
实例方法
- 创建实例
- 通过实例 observe() 监听多个性能条目
- 可以通过 disconnect() 取消监听性能条目
// 创建实例,当记录指定类型的性能条目出现时,性能监测对象的回调函数将会被调用。
const observer = new PerformanceObserver(function (list, obj) {
var entries = list.getEntries();
for (var i = 0; i < entries.length; i++) {
// mark、element 的性能条目会在这里触发
}
});
// 监听指定性能条目,
observer.observe({ type: 'mark' });
// 可设置多个监听
observer.observe({ type: 'element' });
setTimeout(()=> {
// 取消监听
observer.disconnect();
}, 3000)
observer.observe()
针对 observe 方法需要详细再说明下。他支持多个参数
observer.observe({
type: 'navigation',
buffer: true
})
-
type
type 是一个字符串,用于指定您只关心的一种性能条目类型。例如,如果您只关心页面加载(navigation)的性能数据,就可以使用此选项。 -
bufferd
是否缓存加载过的性能条目,这样在 observe 监控调用之前发生的性能条目也会触发回调。必须与 type 选项一起使用。 -
entryTypes 一个字符串对象的数组,每个字符串指定一个要观察的性能条目类型。不能与 “type”、“buffered” 或 “durationThreshold” 选项一起使用。
-
durationThreshold
当你使用 PerformanceObserver API 来观察浏览器的性能条目(performance entries)时,durationThreshold 是一个可选的配置项,它允许你设置一个阈值,以便只接收那些持续时间超过该阈值的条目。- durationThreshold 的默认值是 104ms:这意味着,默认情况下,PerformanceObserver 只会向你报告那些持续时间超过 104 毫秒的性能条目。
- 设置为 16ms 以获取更多交互:由于许多用户交互(如点击、滚动等)的响应时间通常远小于 104 毫秒,因此如果你对这类快速交互感兴趣,你可能需要将 durationThreshold 降低到 16 毫秒或更低。这样做可以让你捕获到更多的交互事件,并了解它们的性能特性。
- 最小 durationThreshold 是 16ms:这是 API 的一个限制,你不能将 durationThreshold 设置为小于 16 毫秒的值。这是因为低于这个值的性能条目可能对于大多数应用来说并不重要,而且过于频繁地触发观察者可能会导致性能下降。
请注意,durationThreshold 主要与那些具有持续时间的性能条目相关,如 longtask、event 类型的条目。对于其他类型的条目(如 mark、measure 等),这个阈值可能不适用或具有不同的含义。
PerformanceEntry
基本介绍
PerformanceEntry是一个通用接口,它定义了一系列属性,如name(性能条目的名称)、entryType(性能条目的类型)、startTime(开始时间戳)、duration(持续时间,如果适用)等,用于描述一个性能事件的各个方面。不同的性能事件(如资源加载、页面渲染等)会生成不同类型的PerformanceEntry对象,这些对象都是PerformanceEntry的子类,各自拥有一些特定的属性和方法。
PerformanceObserver会自动为你捕获PerformanceEntry,并在你指定的回调函数中将它们作为参数传递给你。
每当有匹配的性能事件发生时,PerformanceObserver就会调用你的回调函数,并将一个包含新性能条目的PerformanceObserverEntryList对象作为参数传递给你。你可以通过遍历 PerformanceObserverEntryList.getEntries() 来访问每个PerformanceEntry对象。
const p = new PerformanceObserver(list => {
// PerformanceObserverEntryList 对象
const entries = list.getEntries()
for(let entry of entries) {
console.log('entry:', entry)
}
})
p.observe({
type: 'navigation',
buffered: true
})
一旦你获得了PerformanceEntry对象,你就可以利用它提供的属性和方法来分析和优化你的网页性能了。
FP、FCP / PerformancePaintTiming
接下来我们通过监控具体的性能条目,实现具体的性能指标的记录和计算。有些性能指标可以直接读取例如: first-paint 和 first-contentful-paint
const p = new PerformanceObserver(e => {
e.getEntries().forEach(entry => {
console.log(entry)
})
})
p.observe({
type: 'paint',
buffered: true
})
INP(下一次渲染时间) / PerformanceEventTiming
有些性能指标需要逻辑计算获取例如:INP。
function observeINP() {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries()
let inpTime = 0
// 将所有的事件耗时进行统计,取最大耗时
for (let entry of entries) {
const inputDelay = entry.processingStart - entry.startTime
const processingTime = entry.processingEnd - entry.processingStart
const duration = entry.duration
const eventType = entry.name
console.log('----- INTERACTION -----', eventType)
inpTime = Math.max(inpTime, duration)
}
// INP 时间
console.log('--------total-------' + inpTime)
})
observer.observe({
type: 'event',
buffer: true,
// durationThreshold: 50 // 可设置监听性能条目的阀值,超过50ms 就触发回调 默认是 104ms
})
TTI (Time To Interactive) / PerformanceLongTaskTiming
TTI TTI 指标用于衡量从网页开始加载到其主要子资源加载完成所用的时间,并且能够快速可靠地响应用户输入。 如需根据网页的性能跟踪记录计算 TTI,请按以下步骤操作:
- 从 First Contentful Paint (FCP) 开始。
- 向前搜索一个至少 5 秒的静默窗口,其中静默窗口的定义为:没有长任务,且不超过两个进行中的网络 GET 请求。
- 向后搜索静默窗口之前的最后一个长任务,如果找不到长任务,则停止在 FCP 处停止。
- TTI 是安静窗口之前的最后一个长任务的结束时间(如果未找到长任务,则与 FCP 值相同)。 下图应有助于直观呈现上述步骤:
计算 TTI
function calcTTI() {
console.log('calc calcTTI')
let lcpStartTime = 0 // 记录 LCP 时间
const idleTime = 5000 // 定义空闲时间
const longTaskEntryList = [] // 记录长任务
let getRequestList = [] // 记录 get 请求
let timer
return new Promise((resolve, reject)=> {
const observer = new PerformanceObserver(list => {
try {
const entries = list.getEntries()
const callback = () => {
const afterLcpLongTaskList = longTaskEntryList.filter(entry => entry.startTime >= lcpStartTime)
// 是否有长任务
const lastEntry = afterLcpLongTaskList[afterLcpLongTaskList.length - 1]
// 取最后一个长任务,看是否满足 TTI 条件
if (lastEntry) {
const { startTime, duration } = lastEntry
// 阻塞总时间
const TBT = startTime - lcpStartTime + duration
resolve({TBT, TTI: startTime + duration})
} else {
// LCP 之后没有长任务
resolve(0)
}
timer && clearTimeout(timer)
}
for (entry of entries) {
console.log('---calc-----', entry.entryType, entry)
// 确定 LCP
if (entry.entryType === 'largest-contentful-paint') {
lcpStartTime = entry.startTime
// LCP 可能触发多次
if (timer) {
clearTimeout(timer)
}
// 设定定时器,用于在 5s 空闲时间后执行
// 判断是否为 TTI 条件之一
timer = setTimeout(callback, idleTime)
}
switch (entry.entryType) {
case 'longtask' : {
console.log('longtask')
longTaskEntryList.push(entry)
break
}
case 'resource' : {
const isRequest = entry.initiatorType === 'xmlhttprequest' || entry.initiatorType === 'fetch'
// 只计算 lcp 之后的请求
if (isRequest && getURLList.includes(entry.name) && lcpStartTime) {
getRequestList.push(entry)
}
// 如果累计两个 get 请求,重新设置定时器继续等待
if (getRequestList.length >= 2) {
console.log('存在两个 get 请求',getRequestList)
clearTimeout(timer)
timer = setTimeout(callback, idleTime)
getRequestList = []
}
}
}
}
} catch (error) {
reject(error)
}
})
observer.observe({
type: 'longtask',
buffered: true
})
// 用于计算LCP,TTI 中需要使用
observer.observe({
type: 'largest-contentful-paint',
buffered: true
})
// 用于查看是否存在 GET 请求
observer.observe({
type: 'resource',
buffered: true
})
})
}
其他
完整代码以及其他性能指标可以查看我仓库代码
结语
PerformanceObserver以其非侵入式的监控方式、实时准确的数据捕获、高度自定义的灵活性以及强大的数据处理能力,为我们提供了有效的性能监控体验。结合实际性能指标的计算如 TTI、INP、FP 等可以更好的理解这些指标背后的原理。