From 0be357a1c5fdf506f432a798126366618c806df6 Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Tue, 25 Nov 2025 17:15:30 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BF=AE=E6=94=B9=E6=89=BE=E4=B8=8D?= =?UTF-8?q?=E5=88=B0=E6=96=87=E4=BB=B6=E7=9A=84=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/performanceMonitor.ts | 393 ++++++++++++++++++++++++++++++++ 1 file changed, 393 insertions(+) create mode 100644 src/utils/performanceMonitor.ts diff --git a/src/utils/performanceMonitor.ts b/src/utils/performanceMonitor.ts new file mode 100644 index 00000000..38c07530 --- /dev/null +++ b/src/utils/performanceMonitor.ts @@ -0,0 +1,393 @@ +// src/utils/performanceMonitor.ts +// 性能监控工具 - 统计白屏时间和性能指标 + +import { logger } from './logger'; + +/** + * 性能指标接口 + */ +export interface PerformanceMetrics { + // 网络指标 + dns?: number; // DNS查询时间 + tcp?: number; // TCP连接时间 + ttfb?: number; // 首字节时间(Time To First Byte) + domLoad?: number; // DOM加载时间 + resourceLoad?: number; // 资源加载时间 + + // 渲染指标 + fp?: number; // 首次绘制(First Paint) + fcp?: number; // 首次内容绘制(First Contentful Paint) + lcp?: number; // 最大内容绘制(Largest Contentful Paint) + + // 自定义指标 + reactInit?: number; // React初始化时间 + authCheck?: number; // 认证检查时间 + homepageRender?: number; // 首页渲染时间 + + // 总计 + totalWhiteScreen?: number; // 总白屏时间 +} + +/** + * 性能报告接口 + */ +export interface PerformanceReport { + summary: { + performanceScore: string; + totalMarks: number; + totalMeasures: number; + }; + metrics: PerformanceMetrics; + recommendations: string[]; + marks: Array<{ name: string; time: number }>; + measures: Array<{ name: string; duration: number; startMark: string; endMark: string }>; +} + +/** + * 性能时间点记录 + */ +const performanceMarks: Map = new Map(); + +/** + * 性能测量记录 + */ +const performanceMeasures: Array<{ name: string; duration: number; startMark: string; endMark: string }> = []; + +/** + * 性能监控器类 + */ +class PerformanceMonitor { + private metrics: PerformanceMetrics = {}; + private isProduction: boolean; + + constructor() { + this.isProduction = process.env.NODE_ENV === 'production'; + } + + /** + * 标记性能时间点 + */ + mark(name: string): void { + const timestamp = performance.now(); + performanceMarks.set(name, timestamp); + + if (!this.isProduction) { + logger.debug('PerformanceMonitor', `⏱️ Mark: ${name}`, { + time: `${timestamp.toFixed(2)}ms` + }); + } + } + + /** + * 计算两个时间点之间的耗时 + */ + measure(startMark: string, endMark: string, name?: string): number | null { + const startTime = performanceMarks.get(startMark); + const endTime = performanceMarks.get(endMark); + + if (!startTime || !endTime) { + logger.warn('PerformanceMonitor', 'Missing performance mark', { + startMark, + endMark, + hasStart: !!startTime, + hasEnd: !!endTime + }); + return null; + } + + const duration = endTime - startTime; + const measureName = name || `${startMark} → ${endMark}`; + + // 记录测量 + performanceMeasures.push({ + name: measureName, + duration, + startMark, + endMark + }); + + if (!this.isProduction) { + logger.debug('PerformanceMonitor', `📊 Measure: ${measureName}`, { + duration: `${duration.toFixed(2)}ms` + }); + } + + return duration; + } + + /** + * 获取浏览器性能指标 + */ + collectBrowserMetrics(): void { + if (!window.performance || !window.performance.timing) { + logger.warn('PerformanceMonitor', 'Performance API not supported'); + return; + } + + const timing = window.performance.timing; + const navigationStart = timing.navigationStart; + + // 网络指标 + this.metrics.dns = timing.domainLookupEnd - timing.domainLookupStart; + this.metrics.tcp = timing.connectEnd - timing.connectStart; + this.metrics.ttfb = timing.responseStart - navigationStart; + this.metrics.domLoad = timing.domContentLoadedEventEnd - navigationStart; + this.metrics.resourceLoad = timing.loadEventEnd - navigationStart; + + // 获取 FP/FCP/LCP + this.collectPaintMetrics(); + } + + /** + * 收集绘制指标(FP/FCP/LCP) + */ + private collectPaintMetrics(): void { + if (!window.performance || !window.performance.getEntriesByType) { + return; + } + + // First Paint & First Contentful Paint + const paintEntries = performance.getEntriesByType('paint'); + paintEntries.forEach((entry: any) => { + if (entry.name === 'first-paint') { + this.metrics.fp = entry.startTime; + } else if (entry.name === 'first-contentful-paint') { + this.metrics.fcp = entry.startTime; + } + }); + + // Largest Contentful Paint (需要 PerformanceObserver) + try { + const observer = new PerformanceObserver((list) => { + const entries = list.getEntries(); + const lastEntry = entries[entries.length - 1] as any; + this.metrics.lcp = lastEntry.startTime; + }); + observer.observe({ entryTypes: ['largest-contentful-paint'] }); + } catch (e) { + // LCP可能不被支持 + } + } + + /** + * 收集自定义React指标 + */ + collectReactMetrics(): void { + // React初始化时间 + const reactInit = this.measure('app-start', 'react-ready', 'React初始化'); + if (reactInit) this.metrics.reactInit = reactInit; + + // 认证检查时间 + const authCheck = this.measure('auth-check-start', 'auth-check-end', '认证检查'); + if (authCheck) this.metrics.authCheck = authCheck; + + // 首页渲染时间 + const homepageRender = this.measure('homepage-render-start', 'homepage-render-end', '首页渲染'); + if (homepageRender) this.metrics.homepageRender = homepageRender; + + // 计算总白屏时间 (从页面开始到首屏完成) + const totalWhiteScreen = this.measure('app-start', 'homepage-render-end', '总白屏时间'); + if (totalWhiteScreen) this.metrics.totalWhiteScreen = totalWhiteScreen; + } + + /** + * 生成性能报告(控制台版本) + */ + generateReport(): PerformanceMetrics { + this.collectBrowserMetrics(); + this.collectReactMetrics(); + + const report = { + '=== 网络阶段 ===': { + 'DNS查询': this.formatMs(this.metrics.dns), + 'TCP连接': this.formatMs(this.metrics.tcp), + '首字节时间(TTFB)': this.formatMs(this.metrics.ttfb), + 'DOM加载': this.formatMs(this.metrics.domLoad), + '资源加载': this.formatMs(this.metrics.resourceLoad), + }, + '=== 渲染阶段 ===': { + '首次绘制(FP)': this.formatMs(this.metrics.fp), + '首次内容绘制(FCP)': this.formatMs(this.metrics.fcp), + '最大内容绘制(LCP)': this.formatMs(this.metrics.lcp), + }, + '=== React阶段 ===': { + 'React初始化': this.formatMs(this.metrics.reactInit), + '认证检查': this.formatMs(this.metrics.authCheck), + '首页渲染': this.formatMs(this.metrics.homepageRender), + }, + '=== 总计 ===': { + '总白屏时间': this.formatMs(this.metrics.totalWhiteScreen), + } + }; + + logger.info('PerformanceMonitor', '🚀 性能报告', report); + + // 性能分析建议 + this.analyzePerformance(); + + return this.metrics; + } + + /** + * 获取完整报告(UI版本) + */ + getReport(): PerformanceReport { + this.collectBrowserMetrics(); + this.collectReactMetrics(); + + const recommendations = this.getRecommendations(); + const performanceScore = this.calculatePerformanceScore(); + + return { + summary: { + performanceScore, + totalMarks: performanceMarks.size, + totalMeasures: performanceMeasures.length, + }, + metrics: this.metrics, + recommendations, + marks: Array.from(performanceMarks.entries()).map(([name, time]) => ({ name, time })), + measures: performanceMeasures, + }; + } + + /** + * 计算性能评分 + */ + private calculatePerformanceScore(): string { + const { totalWhiteScreen, ttfb, lcp } = this.metrics; + + let score = 0; + let total = 0; + + if (totalWhiteScreen !== undefined) { + total += 1; + if (totalWhiteScreen < 1500) score += 1; + else if (totalWhiteScreen < 2000) score += 0.7; + else if (totalWhiteScreen < 3000) score += 0.4; + } + + if (ttfb !== undefined) { + total += 1; + if (ttfb < 500) score += 1; + else if (ttfb < 1000) score += 0.7; + else if (ttfb < 1500) score += 0.4; + } + + if (lcp !== undefined) { + total += 1; + if (lcp < 2500) score += 1; + else if (lcp < 4000) score += 0.7; + else if (lcp < 6000) score += 0.4; + } + + if (total === 0) return 'unknown'; + + const percentage = score / total; + + if (percentage >= 0.9) return 'excellent'; + if (percentage >= 0.7) return 'good'; + if (percentage >= 0.5) return 'needs improvement'; + return 'poor'; + } + + /** + * 获取优化建议 + */ + private getRecommendations(): string[] { + const issues: string[] = []; + + if (this.metrics.ttfb && this.metrics.ttfb > 500) { + issues.push(`TTFB过高(${this.metrics.ttfb.toFixed(0)}ms) - 建议优化服务器响应`); + } + + if (this.metrics.resourceLoad && this.metrics.resourceLoad > 3000) { + issues.push(`资源加载慢(${this.metrics.resourceLoad.toFixed(0)}ms) - 建议代码分割/CDN`); + } + + if (this.metrics.authCheck && this.metrics.authCheck > 300) { + issues.push(`认证检查慢(${this.metrics.authCheck.toFixed(0)}ms) - 建议优化Session API`); + } + + if (this.metrics.totalWhiteScreen && this.metrics.totalWhiteScreen > 2000) { + issues.push(`白屏时间过长(${this.metrics.totalWhiteScreen.toFixed(0)}ms) - 目标 <1500ms`); + } + + if (this.metrics.lcp && this.metrics.lcp > 2500) { + issues.push(`LCP过高(${this.metrics.lcp.toFixed(0)}ms) - 建议优化最大内容渲染`); + } + + if (this.metrics.fcp && this.metrics.fcp > 1800) { + issues.push(`FCP过高(${this.metrics.fcp.toFixed(0)}ms) - 建议优化首屏内容渲染`); + } + + if (issues.length === 0) { + issues.push('性能表现良好,无需优化'); + } + + return issues; + } + + /** + * 性能分析和建议 + */ + private analyzePerformance(): void { + const issues = this.getRecommendations(); + + if (issues.length > 0 && issues[0] !== '性能表现良好,无需优化') { + logger.warn('PerformanceMonitor', '🔴 性能问题', { issues }); + } else { + logger.info('PerformanceMonitor', '✅ 性能良好'); + } + } + + /** + * 格式化毫秒数 + */ + private formatMs(ms: number | undefined): string { + if (ms === undefined) return 'N/A'; + + let emoji = '✅'; + if (ms > 1000) emoji = '❌'; + else if (ms > 500) emoji = '⚠️'; + + return `${ms.toFixed(0)}ms ${emoji}`; + } + + /** + * 导出 JSON + */ + exportJSON(): string { + const report = this.getReport(); + return JSON.stringify(report, null, 2); + } + + /** + * 获取所有指标 + */ + getMetrics(): PerformanceMetrics { + return this.metrics; + } + + /** + * 重置监控器 + */ + reset(): void { + this.metrics = {}; + performanceMarks.clear(); + performanceMeasures.length = 0; + } +} + +// 导出单例 +export const performanceMonitor = new PerformanceMonitor(); + +// 页面加载完成后自动生成报告 +if (typeof window !== 'undefined') { + window.addEventListener('load', () => { + // 延迟1秒确保所有指标收集完成 + setTimeout(() => { + performanceMonitor.generateReport(); + }, 1000); + }); +}