UNPKG

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
/** * 指标类型枚举 */ 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: