feat: 修改找不到文件的记录
This commit is contained in:
393
src/utils/performanceMonitor.ts
Normal file
393
src/utils/performanceMonitor.ts
Normal file
@@ -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<string, number> = 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user