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.
1,561 lines (1,550 loc) • 114 kB
JavaScript
/**
* 指标类型枚举
*/
var MetricType;
(function (MetricType) {
MetricType["WEB_VITALS"] = "coreWebVitals";
MetricType["RESOURCES"] = "resources";
MetricType["LONG_TASKS"] = "longTasks";
MetricType["NAVIGATION"] = "navigation";
MetricType["BROWSER_INFO"] = "browserInfo";
})(MetricType || (MetricType = {}));
/**
* Utility functions to check browser support for performance APIs
*/
const browserSupport = {
/**
* Check if the Performance API is supported
*/
hasPerformanceAPI() {
return typeof window !== 'undefined' &&
typeof performance !== 'undefined';
},
/**
* Check if PerformanceObserver is supported
*/
hasPerformanceObserver() {
return typeof window !== 'undefined' &&
typeof PerformanceObserver !== 'undefined';
},
/**
* Check if a specific performance entry type is supported
*/
supportsEntryType(entryType) {
if (!this.hasPerformanceObserver()) {
return false;
}
// In modern browsers, we can check supported entry types
if (PerformanceObserver.supportedEntryTypes) {
return PerformanceObserver.supportedEntryTypes.includes(entryType);
}
// Fallback detection for older browsers
switch (entryType) {
case 'navigation':
return typeof PerformanceNavigationTiming !== 'undefined';
case 'resource':
return typeof PerformanceResourceTiming !== 'undefined';
case 'largest-contentful-paint':
case 'layout-shift':
case 'first-input':
case 'longtask':
// No reliable feature detection for these in older browsers
// Try to create an observer as a test
try {
const observer = new PerformanceObserver(() => { });
observer.observe({ type: entryType, buffered: true });
observer.disconnect();
return true;
}
catch (e) {
return false;
}
default:
return false;
}
}
};
/**
* 日志级别定义
*/
var LogLevel;
(function (LogLevel) {
LogLevel[LogLevel["NONE"] = 0] = "NONE";
LogLevel[LogLevel["ERROR"] = 1] = "ERROR";
LogLevel[LogLevel["WARN"] = 2] = "WARN";
LogLevel[LogLevel["INFO"] = 3] = "INFO";
LogLevel[LogLevel["DEBUG"] = 4] = "DEBUG"; // 输出所有日志,包括调试信息
})(LogLevel || (LogLevel = {}));
// 检测是否为生产环境 - 这将由terser自动删除生产版本中不需要的代码
const IS_DEV = typeof process === 'undefined' || !process.env || process.env.NODE_ENV !== 'production';
/**
* 日志工具类 - 精简版实现,在生产环境会被优化
*/
class Logger {
/**
* 创建日志器实例
* @param options 日志器选项
*/
constructor(options = {}) {
var _a, _b, _c;
this.level = (_a = options.level) !== null && _a !== void 0 ? _a : LogLevel.INFO;
this.prefix = (_b = options.prefix) !== null && _b !== void 0 ? _b : '[PerfObserverKit]';
this.disableInProduction = (_c = options.disableInProduction) !== null && _c !== void 0 ? _c : false; // 默认允许在生产环境输出日志
}
/**
* 设置日志级别
* @param level 日志级别
*/
setLevel(level) {
this.level = level;
// 输出一条日志,确认级别已更改
}
/**
* 设置日志器选项
* @param options 要设置的选项
*/
setOptions(options) {
if (options.level !== undefined) {
this.level = options.level;
}
if (options.prefix !== undefined) {
this.prefix = options.prefix;
}
if (options.disableInProduction !== undefined) {
this.disableInProduction = options.disableInProduction;
}
}
/**
* 输出调试日志
* @param args 日志内容
*/
debug(...args) {
// 移除IS_DEV条件,允许生产环境输出调试信息
if (this.shouldLog(LogLevel.DEBUG)) ;
}
/**
* 输出普通信息日志
* @param args 日志内容
*/
info(...args) {
// 移除IS_DEV条件,允许生产环境输出信息
if (this.shouldLog(LogLevel.INFO)) ;
}
/**
* 输出警告日志
* @param args 日志内容
*/
warn(...args) {
// 警告总是保留,但在生产环境会受日志级别限制
if (this.shouldLog(LogLevel.WARN)) {
console.warn(this.prefix, ...args);
}
}
/**
* 输出错误日志
* @param args 日志内容
*/
error(...args) {
// 错误总是保留,但在生产环境会受日志级别限制
if (this.shouldLog(LogLevel.ERROR)) {
console.error(this.prefix, ...args);
}
}
/**
* 判断是否应该输出指定级别的日志
* @param messageLevel 日志消息级别
* @returns 是否应该输出
*/
shouldLog(messageLevel) {
// 移除自动禁用生产环境日志的功能,改为完全尊重disableInProduction设置
if (!IS_DEV && this.disableInProduction) {
return false;
}
return messageLevel <= this.level;
}
/**
* 获取当前日志配置
* @returns 当前日志配置
*/
getConfiguration() {
return {
level: this.level,
levelName: LogLevel[this.level],
disableInProduction: this.disableInProduction,
isProduction: !IS_DEV
};
}
}
/**
* 全局默认日志器实例
*/
const logger = new Logger();
/**
* 网络性能指标收集器
* 提供收集和分析网络性能相关数据的工具方法
*/
class NetworkMetricsCollector {
/**
* 获取当前网络状态信息
* @returns 网络信息对象
*/
static getNetworkInformation() {
if (typeof navigator !== 'undefined' && 'connection' in navigator) {
const connection = navigator.connection;
if (connection) {
return {
downlink: connection.downlink,
effectiveType: connection.effectiveType,
rtt: connection.rtt,
saveData: connection.saveData
};
}
}
return undefined;
}
/**
* 获取完整的网络上下文信息
* @param extraContext 额外的上下文信息
* @returns 网络上下文信息
*/
static getNetworkContext(extraContext = {}) {
const networkInfo = this.getNetworkInformation();
return {
networkInfo: networkInfo ? {
downlink: networkInfo.downlink,
effectiveType: networkInfo.effectiveType,
rtt: networkInfo.rtt,
saveData: networkInfo.saveData
} : undefined,
url: typeof window !== 'undefined' ? window.location.href : undefined,
visibilityState: typeof document !== 'undefined' ? document.visibilityState : undefined,
...extraContext
};
}
/**
* 计算网络质量评分
* @param networkInfo 网络信息对象
* @returns 'good' | 'needs-improvement' | 'poor'
*/
static rateNetworkQuality(networkInfo) {
if (!networkInfo) {
return 'needs-improvement';
}
// 根据网络类型评估
if (networkInfo.effectiveType === '4g' && networkInfo.downlink && networkInfo.downlink >= 5) {
return 'good';
}
else if (networkInfo.effectiveType === '4g' || networkInfo.effectiveType === '3g') {
return 'needs-improvement';
}
else {
return 'poor';
}
}
}
function calculateTime(end, start) {
return (typeof end === 'number' && typeof start === 'number') ? Math.max(end - start, 0) : 0;
}
/**
* 计算两个时间点之间的差值,确保结果为非负
* @param end 结束时间点
* @param start 开始时间点
* @returns 非负的时间差值
*/
function calculateTimeDelta(end, start) {
const delta = end - start;
return delta > 0 ? delta : 0;
}
/**
* 基础观察者类
* 提供所有Web指标观察者共享的功能
*/
class BaseObserver {
constructor(options) {
this.observer = null;
// 页面可见性和用户交互相关属性
this.isPageVisible = true;
this.userHasInteracted = false;
this.visibilityChangeHandler = null;
this.userInteractionHandler = null;
this.pageshowHandler = null;
this.onUpdate = options.onUpdate;
}
/**
* 启动观察
*/
start() {
this.setupVisibilityTracking();
this.setupUserInteractionTracking();
this.setupPageshowListener();
this.observe();
}
/**
* 停止观察
*/
stop() {
if (this.observer) {
this.observer.disconnect();
this.observer = null;
}
this.cleanupEventListeners();
}
/**
* 清理事件监听器
*/
cleanupEventListeners() {
// 移除页面可见性监听
if (this.visibilityChangeHandler && typeof document !== 'undefined') {
document.removeEventListener('visibilitychange', this.visibilityChangeHandler);
this.visibilityChangeHandler = null;
}
// 移除用户交互监听
if (this.userInteractionHandler && typeof document !== 'undefined') {
document.removeEventListener('click', this.userInteractionHandler);
document.removeEventListener('keydown', this.userInteractionHandler);
this.userInteractionHandler = null;
}
// 移除pageshow监听
if (this.pageshowHandler && typeof window !== 'undefined') {
window.removeEventListener('pageshow', this.pageshowHandler);
this.pageshowHandler = null;
}
}
/**
* 设置页面可见性跟踪
*/
setupVisibilityTracking() {
if (typeof document === 'undefined') {
return;
}
this.isPageVisible = document.visibilityState === 'visible';
this.visibilityChangeHandler = (event) => {
this.isPageVisible = document.visibilityState === 'visible';
logger.debug('页面可见性变化:', this.isPageVisible ? '可见' : '隐藏');
// 通知子类可见性变化
this.onVisibilityChange(this.isPageVisible);
};
document.addEventListener('visibilitychange', this.visibilityChangeHandler);
}
/**
* 设置用户交互跟踪
*/
setupUserInteractionTracking() {
if (typeof document === 'undefined') {
return;
}
this.userInteractionHandler = (event) => {
if (this.userHasInteracted) {
return; // 已经处理过用户交互了,不重复处理
}
this.userHasInteracted = true;
logger.debug('用户已交互');
};
// 监听点击和键盘事件
document.addEventListener('click', this.userInteractionHandler);
document.addEventListener('keydown', this.userInteractionHandler);
}
/**
* 设置bfcache恢复监听
*/
setupPageshowListener() {
if (typeof window === 'undefined') {
return;
}
this.pageshowHandler = (event) => {
// 只有当页面是从bfcache恢复时才处理
if (event.persisted) {
logger.info('页面从bfcache恢复');
// 重置用户交互状态
this.userHasInteracted = false;
// 由子类实现具体的bfcache恢复处理
this.onBFCacheRestore(event);
}
};
window.addEventListener('pageshow', this.pageshowHandler);
}
/**
* 获取网络状态信息
* @returns 网络信息对象
*/
getNetworkInformation() {
return NetworkMetricsCollector.getNetworkInformation();
}
/**
* 获取完整的网络上下文
* @param extraContext 额外的上下文信息
* @returns 完整的上下文数据
*/
getNetworkContext(extraContext = {}) {
// 获取当前页面URL
const currentUrl = typeof window !== 'undefined' ? window.location.href : undefined;
return NetworkMetricsCollector.getNetworkContext({
...extraContext,
userHasInteracted: this.userHasInteracted,
url: currentUrl
});
}
/**
* 计算两个时间点之间的差值,确保结果为非负
* @param end 结束时间点
* @param start 开始时间点
* @returns 非负的时间差值
*/
calculateTimeDelta(end, start) {
return calculateTimeDelta(end, start);
}
/**
* 页面可见性变化时的回调 - 可由子类重写
* @param isVisible 页面是否可见
*/
onVisibilityChange(isVisible) {
// 默认实现为空,由子类覆盖
}
/**
* BFCache恢复处理 - 由子类重写
*/
onBFCacheRestore(event) {
// 默认实现为空,由子类覆盖
}
}
/**
* First Contentful Paint (FCP) 观察者
* 负责测量页面首次内容绘制时间
* 重新实现基于Google Web Vitals库 (https://github.com/GoogleChrome/web-vitals)
*/
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;
/**
* Largest Contentful Paint (LCP) 观察者
* 负责测量页面最大内容绘制时间
* 重新实现基于Google Web Vitals库 (https://github.com/GoogleChrome/web-vitals)
*/
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;
/**
* First Input Delay (FID) 观察者
* 负责测量页面首次输入延迟时间
*/
class FIDObserver extends BaseObserver {
constructor(options) {
super(options);
this.fidObserver = null;
}
/**
* 实现观察FID的方法
*/
observe() {
try {
this.fidObserver = new PerformanceObserver((entryList) => {
const entries = entryList.getEntries();
for (const entry of entries) {
if (entry.name === 'first-input') {
const fidEntry = entry;
const fid = {
name: 'FID',
value: fidEntry.processingStart - fidEntry.startTime,
unit: 'ms',
timestamp: new Date().getTime(),
};
// FID rating thresholds (ms)
if (fid.value <= 100) {
fid.rating = 'good';
}
else if (fid.value <= 300) {
fid.rating = 'needs-improvement';
}
else {
fid.rating = 'poor';
}
this.onUpdate(fid);
// FID is only reported once
if (this.fidObserver) {
this.fidObserver.disconnect();
this.fidObserver = null;
}
break;
}
}
});
this.fidObserver.observe({ type: 'first-input', buffered: true });
}
catch (error) {
logger.error('FID监控不受支持', error);
}
}
/**
* 停止FID观察
*/
stop() {
if (this.fidObserver) {
this.fidObserver.disconnect();
this.fidObserver = null;
}
super.stop();
}
/**
* 页面可见性变化时的回调
* @param isVisible 页面是否可见
*/
onVisibilityChange(isVisible) {
// FID通常是页面加载后的首次输入事件,不需要在可见性变化时特殊处理
if (!isVisible) {
logger.debug('页面隐藏,FID已经收集或仍在等待首次输入事件');
}
else {
logger.debug('页面重新可见,FID状态不变');
}
}
/**
* BFCache恢复处理
* FID通常不需要在bfcache恢复时重新计算
*/
onBFCacheRestore(event) {
// FID不需要特殊处理bfcache恢复
}
}
/**
* Cumulative Layout Shift (CLS) 观察者
* 负责测量页面布局稳定性
* 使用 Google Web Vitals 推荐的会话窗口计算方法
*/
class CLSObserver extends BaseObserver {
constructor(options) {
super(options);
this.clsObserver = null;
// 会话窗口相关属性
this.sessionCount = 0; // 当前会话中的偏移数量
this.sessionValues = [0]; // 各个会话窗口的CLS值
this.sessionGap = 1000; // 会话间隔时间,单位毫秒
this.sessionMax = 5; // 最大会话窗口数量
this.maxSessionEntries = 100; // 每个会话记录的最大偏移数量
// 上次报告的CLS值
this.prevReportedValue = 0;
// 上次偏移的时间戳
this.lastEntryTime = 0;
// 是否需要在页面重新可见时重置会话
this.shouldResetOnNextVisible = false;
// 防抖计时器
this.reportDebounceTimer = null;
// 防抖延迟
this.reportDebounceDelay = 500;
// 初始化首次隐藏时间
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 });
}
/**
* 为CLS指标分配评级
*/
assignCLSRating(value) {
if (value <= CLSObserver.CLS_GOOD_THRESHOLD) {
return 'good';
}
else if (value <= CLSObserver.CLS_NEEDS_IMPROVEMENT_THRESHOLD) {
return 'needs-improvement';
}
else {
return 'poor';
}
}
/**
* 实现观察CLS的方法
*/
observe() {
if (typeof PerformanceObserver === 'undefined') {
logger.error('PerformanceObserver API不可用,无法监控CLS');
return;
}
try {
this.clsObserver = new PerformanceObserver((entryList) => {
const entries = entryList.getEntries();
// 只处理页面在可见状态时发生的布局偏移
if (document.visibilityState !== 'visible') {
logger.debug('页面不可见,忽略布局偏移事件');
return;
}
// 处理新会话的标志
let newSessionStarted = false;
for (const entry of entries) {
// 只计算用户未操作时的布局偏移
const layoutShift = entry;
if (!layoutShift.hadRecentInput && layoutShift.startTime < this.firstHiddenTime) {
// 获取偏移发生的时间戳
const currentTime = layoutShift.startTime;
// 判断是否需要开始新会话
if (this.shouldResetOnNextVisible || currentTime - this.lastEntryTime > this.sessionGap) {
this.startNewSession(layoutShift.value);
newSessionStarted = true;
this.shouldResetOnNextVisible = false;
}
else {
// 累加到当前会话,但限制记录的事件数量
if (this.sessionCount < this.maxSessionEntries) {
this.sessionCount++;
// 累加到当前会话
const currentSessionIndex = this.sessionValues.length - 1;
this.sessionValues[currentSessionIndex] += layoutShift.value;
}
}
// 更新最后一次偏移时间
this.lastEntryTime = currentTime;
// 防抖处理,减少频繁报告
this.debouncedReportCLS();
}
}
// 如果开始了新会话,立即报告一次,不需要防抖
if (newSessionStarted) {
this.reportCLS(this.calculateCLS());
}
});
// 使用buffered选项确保不会丢失之前的布局偏移
this.clsObserver.observe({ type: 'layout-shift', buffered: true });
logger.debug('CLS观察者已启动,开始监控布局偏移');
}
catch (error) {
logger.error('CLS监控不受支持', error);
}
}
/**
* 开始新的会话窗口
* @param initialValue 初始偏移值
*/
startNewSession(initialValue) {
// 添加新会话
this.sessionValues.push(initialValue);
this.sessionCount = 1;
// 如果会话窗口超过限制,移除最小的会话
if (this.sessionValues.length > this.sessionMax) {
// 找到最小的会话值及其索引
let minValue = Infinity;
let minIndex = 0;
for (let i = 0; i < this.sessionValues.length; i++) {
if (this.sessionValues[i] < minValue) {
minValue = this.sessionValues[i];
minIndex = i;
}
}
// 移除最小的会话
this.sessionValues.splice(minIndex, 1);
}
logger.debug(`开始新的CLS会话,当前会话数: ${this.sessionValues.length}`);
}
/**
* 防抖报告CLS,减少频繁更新
*/
debouncedReportCLS() {
// 清除现有计时器
if (this.reportDebounceTimer !== null) {
window.clearTimeout(this.reportDebounceTimer);
}
// 设置新计时器
this.reportDebounceTimer = window.setTimeout(() => {
const clsValue = this.calculateCLS();
// 只有当CLS值显著变化时才报告
if (Math.abs(clsValue - this.prevReportedValue) >= 0.01) {
this.reportCLS(clsValue);
}
this.reportDebounceTimer = null;
}, this.reportDebounceDelay);
}
/**
* 计算最终CLS值
* 取所有会话窗口中的最大值
*/
calculateCLS() {
return Math.max(...this.sessionValues);
}
/**
* 报告CLS指标
*/
reportCLS(clsValue) {
const cls = {
name: 'CLS',
value: clsValue,
unit: '', // CLS没有单位,是无量纲数值
timestamp: new Date().getTime(),
url: typeof window !== 'undefined' ? window.location.href : undefined,
// 添加网络信息和其他上下文
context: {
shiftCount: this.sessionCount,
sessionCount: this.sessionValues.length,
sessionValues: [...this.sessionValues],
largestSession: this.calculateCLS(),
isPageVisible: document.visibilityState === 'visible',
firstHiddenTime: this.firstHiddenTime === Infinity ? null : this.firstHiddenTime
}
};
// 设置评级
cls.rating = this.assignCLSRating(cls.value);
logger.debug(`报告CLS值: ${cls.value},评级: ${cls.rating},页面可见性: ${document.visibilityState}`);
this.onUpdate(cls);
// 更新上次报告的值
this.prevReportedValue = clsValue;
}
/**
* 停止CLS观察
*/
stop() {
// 清除防抖计时器
if (this.reportDebounceTimer !== null) {
window.clearTimeout(this.reportDebounceTimer);
this.reportDebounceTimer = null;
}
if (this.clsObserver) {
this.clsObserver.disconnect();
this.clsObserver = null;
}
super.stop();
}
/**
* 页面可见性变化时的回调
* @param isVisible 页面是否可见
*/
onVisibilityChange(isVisible) {
if (!isVisible) {
// 页面隐藏时,报告当前的CLS值
if (this.firstHiddenTime === Infinity) {
this.firstHiddenTime = performance.now();
logger.debug(`页面隐藏,更新firstHiddenTime: ${this.firstHiddenTime}ms`);
}
const clsValue = this.calculateCLS();
// 无论大小变化,都在页面隐藏时报告一次
this.reportCLS(clsValue);
// 标记需要在页面重新可见时开始新会话
this.shouldResetOnNextVisible = true;
}
else {
// 页面重新变为可见
if (this.shouldResetOnNextVisible) {
logger.debug('页面重新可见,准备开始新的CLS会话');
// 实际的重置会在下一个布局偏移发生时生效
}
}
}
/**
* BFCache恢复处理
* CLS应该重置累积值
*/
onBFCacheRestore(event) {
// 重置CLS会话值
this.sessionValues = [0];
this.sessionCount = 0;
this.prevReportedValue = 0;
this.lastEntryTime = 0;
// 清除计时器
if (this.reportDebounceTimer !== null) {
window.clearTimeout(this.reportDebounceTimer);
this.reportDebounceTimer = null;
}
// 重置firstHiddenTime
this.firstHiddenTime = this.initFirstHiddenTime();
this.setupFirstHiddenTimeListener();
logger.info('CLS值已在bfcache恢复后重置');
// 重新开始CLS监测
if (this.clsObserver) {
this.clsObserver.observe({ type: 'layout-shift', buffered: true });
}
}
}
// CLS评分阈值
CLSObserver.CLS_GOOD_THRESHOLD = 0.1;
CLSObserver.CLS_NEEDS_IMPROVEMENT_THRESHOLD = 0.25;
/**
* Interaction to Next Paint (INP) 观察者
* 负责测量页面交互响应性能
* 实现基于 Google Web Vitals 推荐方法
*/
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;
/**
* 核心Web指标观察者
* 负责监控所有Core Web Vitals指标
*/
class CoreWebVitalsObserver {
constructor(options) {
this.metrics = {};
// 各个指标的观察者
this.fcpObserver = null;
this.lcpObserver = null;
this.fidObserver = null;
this.clsObserver = null;
this.inpObserver = null;
this.onUpdate = options.onUpdate;
this.options = {
// 默认启用
enabled: options.enabled !== undefined ? options.enabled : false,
// FCP和LCP默认启用,其他指标默认不启用
fcp: options.fcp !== undefined ? options.fcp : false,
lcp: options.lcp !== undefined ? options.lcp : false,
fid: options.fid !== undefined ? options.fid : false,
cls: options.cls !== undefined ? options.cls : false,
inp: options.inp !== undefined ? options.inp : false,
// 其他选项
backgroundLoadThreshold: options.backgroundLoadThreshold,
...options
};
logger.debug('核心Web指标观察者已创建,初始配置:', {
enabled: this.options.enabled,
fcp: this.options.fcp,
lcp: this.options.lcp,
fid: this.options.fid,
cls: this.options.cls,
inp: this.options.inp
});
}
/**
* 开始监控所有核心Web指标
*/
start() {
logger.info('开始监控核心Web指标');
// 启动FCP监测
if (this.options.fcp) {
logger.debug('启动FCP监测');
this.startFCPMonitoring();
}
// 启动LCP监测
if (this.options.lcp) {
logger.debug('启动LCP监测');
this.startLCPMonitoring();
}
// 启动FID监测
if (this.options.fid) {
logger.debug('启动FID监测');
this.startFIDMonitoring();
}
// 启动CLS监测
if (this.options.cls) {
logger.debug('启动CLS监测');
this.startCLSMonitoring();
}
// 启动INP监测
if (this.options.inp) {
logger.debug('启动INP监测');
this.startINPMonitoring();
}
logger.debug('核心Web指标监控启动完成');
}
/**
* 停止所有监控
*/
stop() {
logger.info('停止所有核心Web指标监控');
if (this.fcpObserver) {
this.fcpObserver.stop();
this.fcpObserver = null;
logger.debug('FCP监控已停止');
}
if (this.lcpObserver) {
this.lcpObserver.stop();
this.lcpObserver = null;
logger.debug('LCP监控已停止');
}
if (this.fidObserver) {
this.fidObserver.stop();
this.fidObserver = null;
logger.debug('FID监控已停止');
}
if (this.clsObserver) {
this.clsObserver.stop();
this.clsObserver = null;
logger.debug('CLS监控已停止');
}
if (this.inpObserver) {
this.inpObserver.stop();
this.inpObserver = null;
logger.debug('INP监控已停止');
}
logger.debug('所有核心Web指标监控均已停止');
}
/**
* 发送指标更新通知
*/
notifyMetricsUpdate() {
logger.debug('发送核心Web指标更新通知');
this.onUpdate(this.metrics);
}
/**
* 启动FCP监测
*/
startFCPMonitoring() {
try {
this.fcpObserver = new FCPObserver({
onUpdate: