perf-observer-kit
Version:
A lightweight, flexible library for monitoring web performance metrics including Core Web Vitals, resource loading performance, long tasks, and navigation timing.
225 lines • 8.67 kB
JavaScript
import { BaseObserver } from './base-observer';
import { logger } from '../../utils/logger';
/**
* Interaction to Next Paint (INP) 观察者
* 负责测量页面交互响应性能
* 实现基于 Google Web Vitals 推荐方法
*/
export class INPObserver extends BaseObserver {
constructor(options) {
super(options);
this.inpObserver = null;
// 存储所有交互事件的持续时间
this.interactionEvents = [];
// 上次报告的INP值
this.lastReportedINP = 0;
// 报告频率控制
this.minReportingChange = 10; // 最小报告变化阈值(毫秒)
// 初始化首次隐藏时间
this.firstHiddenTime = this.initFirstHiddenTime();
// 监听visibility变化以更新首次隐藏时间
this.setupFirstHiddenTimeListener();
}
/**
* 获取页面首次隐藏的时间
*/
initFirstHiddenTime() {
// 如果页面已经是隐藏状态,返回0
if (typeof document !== 'undefined' && document.visibilityState === 'hidden') {
return 0;
}
// 否则返回无限大,表示页面尚未隐藏
return Infinity;
}
/**
* 设置监听页面首次隐藏的事件
*/
setupFirstHiddenTimeListener() {
if (typeof document === 'undefined')
return;
const updateHiddenTime = () => {
if (document.visibilityState === 'hidden' && this.firstHiddenTime === Infinity) {
this.firstHiddenTime = performance.now();
logger.debug(`记录页面首次隐藏时间: ${this.firstHiddenTime}ms`);
}
};
// 监听页面visibility变化
document.addEventListener('visibilitychange', updateHiddenTime, { once: true });
// 页面卸载时也视为隐藏
document.addEventListener('unload', updateHiddenTime, { once: true });
}
/**
* 为INP指标分配评级
*/
assignINPRating(value) {
if (value <= INPObserver.INP_GOOD_THRESHOLD) {
return 'good';
}
else if (value <= INPObserver.INP_NEEDS_IMPROVEMENT_THRESHOLD) {
return 'needs-improvement';
}
else {
return 'poor';
}
}
/**
* 实现观察INP的方法
*/
observe() {
if (typeof PerformanceObserver === 'undefined') {
logger.error('PerformanceObserver API不可用,无法监控INP');
return;
}
try {
// 定义要观察的交互类型
const eventTypes = ['click', 'keydown', 'pointerdown'];
this.inpObserver = new PerformanceObserver((entryList) => {
// 只处理页面在可见状态时发生的交互
if (document.visibilityState !== 'visible')
return;
const events = entryList.getEntries();
// 处理每个交互事件
for (const event of events) {
// 只处理发生在页面可见状态的交互
if (event.startTime < this.firstHiddenTime) {
const timing = event;
// 检查事件类型是否在我们关注的范围内
if (eventTypes.includes(timing.name)) {
this.interactionEvents.push({
duration: timing.duration,
name: timing.name,
startTime: timing.startTime
});
// 计算并可能报告新的INP值
this.calculateAndReportINP();
}
}
}
});
// 使用类型断言处理非标准属性
try {
// 尝试使用带有durationThreshold的observe
// durationThreshold是较新浏览器中可用的非标准属性
this.inpObserver.observe({
type: 'event',
buffered: true,
// 使用类型断言来处理非标准属性
...{ durationThreshold: 16 } // 只测量至少持续16ms的事件
});
}
catch (error) {
// 回退到不带durationThreshold的observe
this.inpObserver.observe({
type: 'event',
buffered: true
});
logger.warn('浏览器不支持durationThreshold参数,使用默认配置');
}
}
catch (error) {
logger.error('INP监控不受支持', error);
}
}
/**
* 计算INP(交互到下一次绘制)值
* 使用Google推荐的方法(取第75百分位数)
*/
calculateINP() {
if (this.interactionEvents.length === 0) {
return 0;
}
// 将交互事件按持续时间排序
const sortedDurations = this.interactionEvents.map(event => event.duration).sort((a, b) => a - b);
// 计算第75百分位数
const percentile = 0.75;
const index = Math.floor(sortedDurations.length * percentile);
// 如果只有一个交互,直接返回
if (sortedDurations.length === 1) {
return sortedDurations[0];
}
// 返回第75百分位数的值
return sortedDurations[index];
}
/**
* 计算并报告INP值,如果值有显著变化
*/
calculateAndReportINP() {
// 计算新的INP值
const inpValue = this.calculateINP();
// 只有当INP值显著变化时才报告
if (Math.abs(inpValue - this.lastReportedINP) >= this.minReportingChange) {
// 创建INP指标对象
const inp = {
name: 'INP',
value: inpValue,
unit: 'ms',
timestamp: new Date().getTime(),
url: typeof window !== 'undefined' ? window.location.href : undefined,
context: {
interactionCount: this.interactionEvents.length,
percentile: 75,
highestDuration: Math.max(...this.interactionEvents.map(e => e.duration)),
medianDuration: this.interactionEvents.length > 0
? this.interactionEvents.map(e => e.duration).sort((a, b) => a - b)[Math.floor(this.interactionEvents.length / 2)]
: 0,
eventTypes: [...new Set(this.interactionEvents.map(e => e.name))],
firstHiddenTime: this.firstHiddenTime === Infinity ? null : this.firstHiddenTime
}
};
// 设置评级
inp.rating = this.assignINPRating(inp.value);
logger.debug(`报告INP值: ${inp.value}ms,评级: ${inp.rating},基于${this.interactionEvents.length}个交互`);
this.onUpdate(inp);
// 更新最后报告的值
this.lastReportedINP = inpValue;
}
}
/**
* 停止INP观察
*/
stop() {
if (this.inpObserver) {
this.inpObserver.disconnect();
this.inpObserver = null;
}
super.stop();
}
/**
* 页面可见性变化时的回调
* @param isVisible 页面是否可见
*/
onVisibilityChange(isVisible) {
// 更新firstHiddenTime
if (!isVisible && this.firstHiddenTime === Infinity) {
this.firstHiddenTime = performance.now();
logger.debug(`页面隐藏,更新firstHiddenTime: ${this.firstHiddenTime}ms`);
// 当页面隐藏时,报告当前的INP值
if (this.interactionEvents.length > 0) {
this.calculateAndReportINP();
}
}
}
/**
* BFCache恢复处理
* 重置INP监测
*/
onBFCacheRestore(event) {
// 重置INP值和交互事件
this.interactionEvents = [];
this.lastReportedINP = 0;
// 重置firstHiddenTime
this.firstHiddenTime = this.initFirstHiddenTime();
this.setupFirstHiddenTimeListener();
logger.info('INP监测已在bfcache恢复后重置');
// 重新启动INP监测
if (this.inpObserver) {
this.inpObserver.disconnect();
this.inpObserver = null;
}
this.observe();
}
}
// INP评分阈值(毫秒)
INPObserver.INP_GOOD_THRESHOLD = 200;
INPObserver.INP_NEEDS_IMPROVEMENT_THRESHOLD = 500;
//# sourceMappingURL=inp-observer.js.map