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.
190 lines • 7.33 kB
JavaScript
import { BaseObserver } from './base-observer';
import { logger } from '../../utils/logger';
/**
* First Contentful Paint (FCP) 观察者
* 负责测量页面首次内容绘制时间
* 重新实现基于Google Web Vitals库 (https://github.com/GoogleChrome/web-vitals)
*/
export class FCPObserver extends BaseObserver {
constructor(options) {
super(options);
this.fcpObserver = 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;
}
/**
* 为FCP指标分配评级
*/
assignFCPRating(value) {
if (value <= FCPObserver.FCP_GOOD_THRESHOLD) {
return 'good';
}
else if (value <= FCPObserver.FCP_NEEDS_IMPROVEMENT_THRESHOLD) {
return 'needs-improvement';
}
else {
return 'poor';
}
}
/**
* 实现观察FCP的方法
*/
observe() {
if (typeof PerformanceObserver === 'undefined') {
logger.error('PerformanceObserver API不可用,无法监控FCP');
return;
}
try {
this.fcpObserver = new PerformanceObserver((entryList) => {
const entries = entryList.getEntries();
for (const entry of entries) {
if (entry.name === 'first-contentful-paint') {
// 只有当页面在FCP发生前未隐藏时才报告
if (entry.startTime < this.firstHiddenTime) {
// 计算FCP值,考虑activationStart(预渲染)
const fcpValue = Math.max(entry.startTime - this.getActivationStart(), 0);
const fcp = {
name: 'FCP',
value: fcpValue,
unit: 'ms',
timestamp: new Date().getTime(),
url: typeof window !== 'undefined' ? window.location.href : undefined,
networkMetrics: this.getNetworkInformation()
};
// 设置评级
fcp.rating = this.assignFCPRating(fcp.value);
this.onUpdate(fcp);
// 标记指标已报告
this.metricReported = true;
}
else {
logger.debug('页面在FCP前已隐藏,忽略此FCP事件');
}
// FCP只报告一次
if (this.fcpObserver) {
this.fcpObserver.disconnect();
this.fcpObserver = null;
}
break;
}
}
});
this.fcpObserver.observe({ type: 'paint', buffered: true });
}
catch (error) {
logger.error('FCP监控不受支持', error);
}
}
/**
* 停止FCP观察
*/
stop() {
if (this.fcpObserver) {
this.fcpObserver.disconnect();
this.fcpObserver = null;
}
super.stop();
}
/**
* 页面可见性变化时的回调
* @param isVisible 页面是否可见
*/
onVisibilityChange(isVisible) {
if (!isVisible && this.firstHiddenTime === Infinity) {
this.firstHiddenTime = performance.now();
logger.debug(`页面隐藏,更新firstHiddenTime: ${this.firstHiddenTime}ms`);
}
}
/**
* BFCache恢复处理
* 在bfcache恢复后,使用双RAF测量时间为新的FCP值
*/
onBFCacheRestore(event) {
// 对于BFCache恢复,我们需要新的FCP测量
// 重置状态
this.metricReported = false;
this.firstHiddenTime = this.initFirstHiddenTime();
this.setupFirstHiddenTimeListener();
// 创建一个新的FCP指标,使用恢复时间差作为值
const restoreTime = event.timeStamp;
// 使用双重requestAnimationFrame确保我们在浏览器绘制后进行测量
const doubleRAF = (callback) => {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
callback();
});
});
};
doubleRAF(() => {
const currentTime = performance.now();
const timeFromRestore = currentTime - restoreTime;
const fcp = {
name: 'FCP',
value: timeFromRestore,
unit: 'ms',
timestamp: currentTime,
url: typeof window !== 'undefined' ? window.location.href : undefined,
context: {
bfcacheRestore: true,
restoreTime: restoreTime
}
};
fcp.rating = this.assignFCPRating(fcp.value);
logger.info(`从bfcache恢复到现在的时间: ${timeFromRestore}ms,作为新的FCP值`);
this.onUpdate(fcp);
this.metricReported = true;
});
// 重新观察FCP,以防有更早的绘制事件
this.observe();
}
}
// FCP评分阈值(毫秒)
FCPObserver.FCP_GOOD_THRESHOLD = 1800;
FCPObserver.FCP_NEEDS_IMPROVEMENT_THRESHOLD = 3000;
//# sourceMappingURL=fcp-observer.js.map