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.
265 lines • 10.7 kB
JavaScript
import { BaseObserver } from './base-observer';
import { logger } from '../../utils/logger';
/**
* Largest Contentful Paint (LCP) 观察者
* 负责测量页面最大内容绘制时间
* 重新实现基于Google Web Vitals库 (https://github.com/GoogleChrome/web-vitals)
*/
export class LCPObserver extends BaseObserver {
constructor(options) {
super(options);
this.lcpObserver = null;
// 记录指标是否已上报
this.metricReported = false;
// 初始化首次隐藏时间
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 });
}
/**
* 获取激活开始时间
*/
getActivationStart() {
if (typeof performance === 'undefined')
return 0;
const entries = performance.getEntriesByType('navigation');
if (!entries || entries.length === 0)
return 0;
const navigationEntry = entries[0];
// activationStart是非标准属性,在某些浏览器中可用
const activationStart = navigationEntry === null || navigationEntry === void 0 ? void 0 : navigationEntry.activationStart;
return activationStart ? activationStart : 0;
}
/**
* 实现观察LCP的方法
*/
observe() {
this.startLCPMonitoring();
}
/**
* 为LCP指标分配评级
*/
assignLCPRating(value) {
if (value <= LCPObserver.LCP_GOOD_THRESHOLD) {
return 'good';
}
else if (value <= LCPObserver.LCP_NEEDS_IMPROVEMENT_THRESHOLD) {
return 'needs-improvement';
}
else {
return 'poor';
}
}
/**
* 启动LCP监控
*/
startLCPMonitoring() {
if (typeof PerformanceObserver === 'undefined') {
logger.error('PerformanceObserver API不可用,无法监控LCP');
return;
}
try {
this.lcpObserver = new PerformanceObserver((entryList) => {
// 获取所有entries,但如果不需要报告所有变化,只考虑最后一个
const entries = entryList.getEntries();
// 遍历entries(通常只有一个)
entries.forEach(entry => {
// 只有当页面在LCP发生前未隐藏时才报告
if (entry.startTime < this.firstHiddenTime) {
// 计算LCP值,考虑activationStart(预渲染)
const lcpValue = Math.max(entry.startTime - this.getActivationStart(), 0);
// 创建LCP指标
const lcp = {
name: 'LCP',
value: lcpValue,
unit: 'ms',
timestamp: new Date().getTime(),
url: typeof window !== 'undefined' ? window.location.href : undefined,
networkMetrics: this.getNetworkInformation(),
context: {
elementId: entry.element ? entry.element.id || null : null,
elementTagName: entry.element ? entry.element.tagName || null : null,
elementType: entry.element ? entry.element.type || null : null,
size: entry.size || 0
}
};
// 设置评级
lcp.rating = this.assignLCPRating(lcp.value);
// 发送指标更新
this.onUpdate(lcp);
}
else {
logger.debug('页面在LCP前已隐藏,忽略此LCP事件');
}
});
});
// 启动观察
this.lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true });
// 设置停止监听的条件
this.setupStopListening();
}
catch (error) {
logger.error('LCP监控不受支持', error);
}
}
/**
* 设置停止监听的各种条件
*/
setupStopListening() {
// 用于确保只报告一次的函数
const stopListening = () => {
if (this.metricReported || !this.lcpObserver)
return;
// 处理所有剩余记录
const records = this.lcpObserver.takeRecords();
if (records && records.length > 0) {
const lastEntry = records[records.length - 1];
if (lastEntry && lastEntry.startTime < this.firstHiddenTime) {
const lcpValue = Math.max(lastEntry.startTime - this.getActivationStart(), 0);
const lcp = {
name: 'LCP',
value: lcpValue,
unit: 'ms',
timestamp: new Date().getTime(),
url: typeof window !== 'undefined' ? window.location.href : undefined,
networkMetrics: this.getNetworkInformation(),
context: {
elementId: lastEntry.element ? lastEntry.element.id || null : null,
elementTagName: lastEntry.element ? lastEntry.element.tagName || null : null,
elementType: lastEntry.element ? lastEntry.element.type || null : null,
size: lastEntry.size || 0,
finalReport: true
}
};
lcp.rating = this.assignLCPRating(lcp.value);
this.onUpdate(lcp);
}
}
// 断开观察器连接
this.lcpObserver.disconnect();
this.lcpObserver = null;
this.metricReported = true;
logger.debug('LCP测量完成,指标已报告');
};
// 监听用户交互事件(键盘和点击)
const addInteractionListener = (type) => {
if (typeof document === 'undefined')
return;
document.addEventListener(type, () => {
// 使用setTimeout将回调放入单独的任务中,减少对INP的影响
setTimeout(() => {
logger.debug(`检测到用户${type}交互,停止LCP监听`);
stopListening();
}, 0);
}, { once: true, capture: true });
};
['keydown', 'click'].forEach(addInteractionListener);
// 监听页面隐藏事件
if (typeof document !== 'undefined') {
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
logger.debug('页面隐藏,停止LCP监听');
stopListening();
}
});
}
}
/**
* 停止LCP观察
*/
stop() {
if (this.lcpObserver) {
this.lcpObserver.disconnect();
this.lcpObserver = null;
}
super.stop();
}
/**
* 页面可见性变化时的回调
* @param isVisible 页面是否可见
*/
onVisibilityChange(isVisible) {
if (!isVisible && this.firstHiddenTime === Infinity) {
this.firstHiddenTime = performance.now();
logger.debug(`页面隐藏,更新firstHiddenTime: ${this.firstHiddenTime}ms`);
}
}
/**
* BFCache恢复处理
* 在bfcache恢复后重新计算从恢复到当前的时间差作为新的LCP
*/
onBFCacheRestore(event) {
// 断开现有的LCP观察器
if (this.lcpObserver) {
this.lcpObserver.disconnect();
this.lcpObserver = null;
}
// 重置状态
this.metricReported = false;
this.firstHiddenTime = this.initFirstHiddenTime();
this.setupFirstHiddenTimeListener();
// 创建一个新的LCP指标,使用恢复时间差作为值
const restoreTime = event.timeStamp;
// 使用双重requestAnimationFrame确保我们在浏览器绘制后进行测量
const doubleRAF = (callback) => {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
callback();
});
});
};
doubleRAF(() => {
const currentTime = performance.now();
const timeFromRestore = currentTime - restoreTime;
const lcp = {
name: 'LCP',
value: timeFromRestore,
unit: 'ms',
timestamp: currentTime,
url: typeof window !== 'undefined' ? window.location.href : undefined,
context: {
bfcacheRestore: true,
restoreTime: restoreTime
}
};
lcp.rating = this.assignLCPRating(lcp.value);
logger.info(`从bfcache恢复到现在的时间: ${timeFromRestore}ms,作为新的LCP值`);
this.onUpdate(lcp);
this.metricReported = true;
});
// 重新开始LCP监测以捕获后续可能的更大内容
this.startLCPMonitoring();
}
}
// LCP评分阈值(毫秒)
LCPObserver.LCP_GOOD_THRESHOLD = 2500;
LCPObserver.LCP_NEEDS_IMPROVEMENT_THRESHOLD = 4000;
//# sourceMappingURL=lcp-observer.js.map