feat:1️⃣ 增强 performanceMonitor.ts
- ✅ 新增 measure(name, startMark, endMark) 方法(支持命名测量) - ✅ 新增 getMarks() - 获取所有性能标记 - ✅ 新增 getMeasures() - 获取所有测量结果 - ✅ 新增 getReport() - 返回完整 JSON 报告 - ✅ 新增 exportJSON() - 导出 JSON 文件 - ✅ 新增 reportToPostHog() - 上报到 PostHog - ✅ 新增全局 API window.__PERFORMANCE__(仅开发环境) - ✅ 彩色控制台使用说明 2️⃣ 添加 PostHog 性能上报 - ✅ 在 posthog.js 中新增 reportPerformanceMetrics() 函数 - ✅ 上报所有关键性能指标(网络、渲染、React) - ✅ 自动计算性能评分(0-100) - ✅ 包含浏览器和设备信息
This commit is contained in:
@@ -21,6 +21,7 @@ import AppProviders from './providers/AppProviders';
|
||||
|
||||
// Components
|
||||
import GlobalComponents from './components/GlobalComponents';
|
||||
import { PerformancePanel } from './components/PerformancePanel';
|
||||
|
||||
// Hooks
|
||||
import { useGlobalErrorHandler } from './hooks/useGlobalErrorHandler';
|
||||
@@ -132,6 +133,7 @@ export default function App() {
|
||||
<AppProviders>
|
||||
<AppContent />
|
||||
<GlobalComponents />
|
||||
<PerformancePanel />
|
||||
</AppProviders>
|
||||
);
|
||||
}
|
||||
384
src/components/PerformancePanel.tsx
Normal file
384
src/components/PerformancePanel.tsx
Normal file
@@ -0,0 +1,384 @@
|
||||
// src/components/PerformancePanel.tsx
|
||||
// 性能监控可视化面板 - 仅开发环境显示
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
VStack,
|
||||
HStack,
|
||||
Text,
|
||||
Badge,
|
||||
Button,
|
||||
IconButton,
|
||||
Stat,
|
||||
StatLabel,
|
||||
StatNumber,
|
||||
StatHelpText,
|
||||
Accordion,
|
||||
AccordionItem,
|
||||
AccordionButton,
|
||||
AccordionPanel,
|
||||
AccordionIcon,
|
||||
useDisclosure,
|
||||
Drawer,
|
||||
DrawerBody,
|
||||
DrawerHeader,
|
||||
DrawerOverlay,
|
||||
DrawerContent,
|
||||
DrawerCloseButton,
|
||||
} from '@chakra-ui/react';
|
||||
import { MdSpeed, MdClose, MdRefresh, MdFileDownload } from 'react-icons/md';
|
||||
import { performanceMonitor } from '@/utils/performanceMonitor';
|
||||
|
||||
/**
|
||||
* 性能评分颜色映射
|
||||
*/
|
||||
const getScoreColor = (score: string): string => {
|
||||
switch (score) {
|
||||
case 'excellent':
|
||||
return 'green';
|
||||
case 'good':
|
||||
return 'blue';
|
||||
case 'needs improvement':
|
||||
return 'yellow';
|
||||
case 'poor':
|
||||
return 'red';
|
||||
default:
|
||||
return 'gray';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化毫秒数
|
||||
*/
|
||||
const formatMs = (ms: number | undefined): string => {
|
||||
if (ms === undefined) return 'N/A';
|
||||
return `${ms.toFixed(0)}ms`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 性能面板组件
|
||||
*/
|
||||
export const PerformancePanel: React.FC = () => {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const [report, setReport] = useState<any>(null);
|
||||
|
||||
// 刷新性能数据
|
||||
const refreshData = () => {
|
||||
const newReport = performanceMonitor.getReport();
|
||||
setReport(newReport);
|
||||
};
|
||||
|
||||
// 导出 JSON
|
||||
const exportJSON = () => {
|
||||
const json = performanceMonitor.exportJSON();
|
||||
const blob = new Blob([json], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `performance-report-${Date.now()}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
// 初始加载
|
||||
useEffect(() => {
|
||||
refreshData();
|
||||
}, []);
|
||||
|
||||
// 仅在开发环境显示
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 浮动按钮 */}
|
||||
<IconButton
|
||||
aria-label="Open performance panel"
|
||||
icon={<MdSpeed />}
|
||||
position="fixed"
|
||||
bottom="20px"
|
||||
right="20px"
|
||||
colorScheme="blue"
|
||||
size="lg"
|
||||
borderRadius="full"
|
||||
boxShadow="lg"
|
||||
zIndex={9999}
|
||||
onClick={onOpen}
|
||||
/>
|
||||
|
||||
{/* 抽屉面板 */}
|
||||
<Drawer isOpen={isOpen} placement="right" onClose={onClose} size="lg">
|
||||
<DrawerOverlay />
|
||||
<DrawerContent>
|
||||
<DrawerCloseButton />
|
||||
<DrawerHeader borderBottomWidth="1px">
|
||||
<HStack>
|
||||
<MdSpeed size={24} />
|
||||
<Text>性能监控面板</Text>
|
||||
</HStack>
|
||||
</DrawerHeader>
|
||||
|
||||
<DrawerBody>
|
||||
{report ? (
|
||||
<VStack spacing={4} align="stretch" py={4}>
|
||||
{/* 操作按钮 */}
|
||||
<HStack spacing={2}>
|
||||
<Button
|
||||
leftIcon={<MdRefresh />}
|
||||
size="sm"
|
||||
colorScheme="blue"
|
||||
onClick={refreshData}
|
||||
>
|
||||
刷新数据
|
||||
</Button>
|
||||
<Button
|
||||
leftIcon={<MdFileDownload />}
|
||||
size="sm"
|
||||
colorScheme="green"
|
||||
onClick={exportJSON}
|
||||
>
|
||||
导出 JSON
|
||||
</Button>
|
||||
</HStack>
|
||||
|
||||
{/* 总览 */}
|
||||
<Box p={4} bg="gray.50" borderRadius="md">
|
||||
<VStack align="stretch" spacing={2}>
|
||||
<HStack justify="space-between">
|
||||
<Text fontWeight="bold">性能评分</Text>
|
||||
<Badge
|
||||
colorScheme={getScoreColor(report.summary.performanceScore)}
|
||||
fontSize="md"
|
||||
px={3}
|
||||
py={1}
|
||||
borderRadius="full"
|
||||
>
|
||||
{report.summary.performanceScore.toUpperCase()}
|
||||
</Badge>
|
||||
</HStack>
|
||||
<HStack justify="space-between">
|
||||
<Text fontSize="sm" color="gray.600">
|
||||
性能标记: {report.summary.totalMarks}
|
||||
</Text>
|
||||
<Text fontSize="sm" color="gray.600">
|
||||
性能测量: {report.summary.totalMeasures}
|
||||
</Text>
|
||||
</HStack>
|
||||
</VStack>
|
||||
</Box>
|
||||
|
||||
{/* 网络指标 */}
|
||||
<Box>
|
||||
<Text fontWeight="bold" mb={2}>
|
||||
网络指标
|
||||
</Text>
|
||||
<VStack align="stretch" spacing={2}>
|
||||
<MetricStat
|
||||
label="DNS 查询"
|
||||
value={formatMs(report.metrics.dns)}
|
||||
threshold={100}
|
||||
actualValue={report.metrics.dns}
|
||||
/>
|
||||
<MetricStat
|
||||
label="TCP 连接"
|
||||
value={formatMs(report.metrics.tcp)}
|
||||
threshold={100}
|
||||
actualValue={report.metrics.tcp}
|
||||
/>
|
||||
<MetricStat
|
||||
label="TTFB"
|
||||
value={formatMs(report.metrics.ttfb)}
|
||||
threshold={500}
|
||||
actualValue={report.metrics.ttfb}
|
||||
/>
|
||||
</VStack>
|
||||
</Box>
|
||||
|
||||
{/* 渲染指标 */}
|
||||
<Box>
|
||||
<Text fontWeight="bold" mb={2}>
|
||||
渲染指标
|
||||
</Text>
|
||||
<VStack align="stretch" spacing={2}>
|
||||
<MetricStat
|
||||
label="FP (首次绘制)"
|
||||
value={formatMs(report.metrics.fp)}
|
||||
threshold={1000}
|
||||
actualValue={report.metrics.fp}
|
||||
/>
|
||||
<MetricStat
|
||||
label="FCP (首次内容绘制)"
|
||||
value={formatMs(report.metrics.fcp)}
|
||||
threshold={1800}
|
||||
actualValue={report.metrics.fcp}
|
||||
/>
|
||||
<MetricStat
|
||||
label="LCP (最大内容绘制)"
|
||||
value={formatMs(report.metrics.lcp)}
|
||||
threshold={2500}
|
||||
actualValue={report.metrics.lcp}
|
||||
/>
|
||||
</VStack>
|
||||
</Box>
|
||||
|
||||
{/* React 指标 */}
|
||||
<Box>
|
||||
<Text fontWeight="bold" mb={2}>
|
||||
React 指标
|
||||
</Text>
|
||||
<VStack align="stretch" spacing={2}>
|
||||
<MetricStat
|
||||
label="React 初始化"
|
||||
value={formatMs(report.metrics.reactInit)}
|
||||
threshold={1000}
|
||||
actualValue={report.metrics.reactInit}
|
||||
/>
|
||||
<MetricStat
|
||||
label="认证检查"
|
||||
value={formatMs(report.metrics.authCheck)}
|
||||
threshold={300}
|
||||
actualValue={report.metrics.authCheck}
|
||||
/>
|
||||
<MetricStat
|
||||
label="首页渲染"
|
||||
value={formatMs(report.metrics.homepageRender)}
|
||||
threshold={500}
|
||||
actualValue={report.metrics.homepageRender}
|
||||
/>
|
||||
</VStack>
|
||||
</Box>
|
||||
|
||||
{/* 总白屏时间 */}
|
||||
<Box p={4} bg="blue.50" borderRadius="md" borderWidth="2px" borderColor="blue.200">
|
||||
<Stat>
|
||||
<StatLabel>总白屏时间</StatLabel>
|
||||
<StatNumber fontSize="3xl">
|
||||
{formatMs(report.metrics.totalWhiteScreen)}
|
||||
</StatNumber>
|
||||
<StatHelpText>
|
||||
{report.metrics.totalWhiteScreen && report.metrics.totalWhiteScreen < 1500
|
||||
? '✅ 优秀'
|
||||
: report.metrics.totalWhiteScreen && report.metrics.totalWhiteScreen < 2000
|
||||
? '⚠️ 良好'
|
||||
: '❌ 需要优化'}
|
||||
</StatHelpText>
|
||||
</Stat>
|
||||
</Box>
|
||||
|
||||
{/* 优化建议 */}
|
||||
<Accordion allowToggle>
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Box flex="1" textAlign="left" fontWeight="bold">
|
||||
优化建议 ({report.recommendations.length})
|
||||
</Box>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel pb={4}>
|
||||
<VStack align="stretch" spacing={2}>
|
||||
{report.recommendations.map((rec: string, index: number) => (
|
||||
<Text key={index} fontSize="sm">
|
||||
{rec}
|
||||
</Text>
|
||||
))}
|
||||
</VStack>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
|
||||
{/* 性能标记 */}
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Box flex="1" textAlign="left" fontWeight="bold">
|
||||
性能标记 ({report.marks.length})
|
||||
</Box>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel pb={4}>
|
||||
<VStack align="stretch" spacing={1}>
|
||||
{report.marks.map((mark: any, index: number) => (
|
||||
<HStack key={index} justify="space-between" fontSize="sm">
|
||||
<Text>{mark.name}</Text>
|
||||
<Text color="gray.600">{mark.time.toFixed(2)}ms</Text>
|
||||
</HStack>
|
||||
))}
|
||||
</VStack>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
|
||||
{/* 性能测量 */}
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Box flex="1" textAlign="left" fontWeight="bold">
|
||||
性能测量 ({report.measures.length})
|
||||
</Box>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel pb={4}>
|
||||
<VStack align="stretch" spacing={2}>
|
||||
{report.measures.map((measure: any, index: number) => (
|
||||
<Box key={index} p={2} bg="gray.50" borderRadius="md">
|
||||
<HStack justify="space-between">
|
||||
<Text fontWeight="semibold" fontSize="sm">
|
||||
{measure.name}
|
||||
</Text>
|
||||
<Badge>{measure.duration.toFixed(2)}ms</Badge>
|
||||
</HStack>
|
||||
<Text fontSize="xs" color="gray.600">
|
||||
{measure.startMark} → {measure.endMark}
|
||||
</Text>
|
||||
</Box>
|
||||
))}
|
||||
</VStack>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</VStack>
|
||||
) : (
|
||||
<Text>加载中...</Text>
|
||||
)}
|
||||
</DrawerBody>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 指标统计组件
|
||||
*/
|
||||
interface MetricStatProps {
|
||||
label: string;
|
||||
value: string;
|
||||
threshold: number;
|
||||
actualValue?: number;
|
||||
}
|
||||
|
||||
const MetricStat: React.FC<MetricStatProps> = ({ label, value, threshold, actualValue }) => {
|
||||
const isGood = actualValue !== undefined && actualValue < threshold;
|
||||
|
||||
return (
|
||||
<HStack justify="space-between" p={2} bg="gray.50" borderRadius="md">
|
||||
<Text fontSize="sm">{label}</Text>
|
||||
<HStack>
|
||||
<Text fontSize="sm" fontWeight="bold">
|
||||
{value}
|
||||
</Text>
|
||||
{actualValue !== undefined && (
|
||||
<Text fontSize="xs">{isGood ? '✅' : '⚠️'}</Text>
|
||||
)}
|
||||
</HStack>
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
|
||||
export default PerformancePanel;
|
||||
@@ -293,4 +293,105 @@ export const isFeatureEnabled = (flagKey) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Report performance metrics to PostHog
|
||||
* @param {object} metrics - Performance metrics object
|
||||
*/
|
||||
export const reportPerformanceMetrics = (metrics) => {
|
||||
// 仅在生产环境上报
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
console.log('📊 [开发环境] 性能指标(未上报到 PostHog):', metrics);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取浏览器和设备信息
|
||||
const browserInfo = {
|
||||
userAgent: navigator.userAgent,
|
||||
viewport: `${window.innerWidth}x${window.innerHeight}`,
|
||||
connection: navigator.connection?.effectiveType || 'unknown',
|
||||
deviceMemory: navigator.deviceMemory || 'unknown',
|
||||
hardwareConcurrency: navigator.hardwareConcurrency || 'unknown',
|
||||
};
|
||||
|
||||
// 上报性能指标
|
||||
posthog.capture('Performance Metrics', {
|
||||
// 网络指标
|
||||
dns_ms: metrics.dns,
|
||||
tcp_ms: metrics.tcp,
|
||||
ttfb_ms: metrics.ttfb,
|
||||
dom_load_ms: metrics.domLoad,
|
||||
resource_load_ms: metrics.resourceLoad,
|
||||
|
||||
// 渲染指标
|
||||
fp_ms: metrics.fp,
|
||||
fcp_ms: metrics.fcp,
|
||||
lcp_ms: metrics.lcp,
|
||||
|
||||
// React 指标
|
||||
react_init_ms: metrics.reactInit,
|
||||
auth_check_ms: metrics.authCheck,
|
||||
homepage_render_ms: metrics.homepageRender,
|
||||
|
||||
// 总计
|
||||
total_white_screen_ms: metrics.totalWhiteScreen,
|
||||
|
||||
// 性能评分
|
||||
performance_score: calculatePerformanceScore(metrics),
|
||||
|
||||
// 浏览器和设备信息
|
||||
...browserInfo,
|
||||
|
||||
// 时间戳
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
console.log('✅ 性能指标已上报到 PostHog');
|
||||
} catch (error) {
|
||||
console.error('❌ PostHog 性能指标上报失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate overall performance score (0-100)
|
||||
* @param {object} metrics - Performance metrics
|
||||
* @returns {number} Score from 0 to 100
|
||||
*/
|
||||
const calculatePerformanceScore = (metrics) => {
|
||||
let score = 100;
|
||||
|
||||
// 白屏时间评分(权重 40%)
|
||||
if (metrics.totalWhiteScreen) {
|
||||
if (metrics.totalWhiteScreen > 3000) score -= 40;
|
||||
else if (metrics.totalWhiteScreen > 2000) score -= 20;
|
||||
else if (metrics.totalWhiteScreen > 1500) score -= 10;
|
||||
}
|
||||
|
||||
// TTFB 评分(权重 20%)
|
||||
if (metrics.ttfb) {
|
||||
if (metrics.ttfb > 1000) score -= 20;
|
||||
else if (metrics.ttfb > 500) score -= 10;
|
||||
}
|
||||
|
||||
// LCP 评分(权重 20%)
|
||||
if (metrics.lcp) {
|
||||
if (metrics.lcp > 4000) score -= 20;
|
||||
else if (metrics.lcp > 2500) score -= 10;
|
||||
}
|
||||
|
||||
// FCP 评分(权重 10%)
|
||||
if (metrics.fcp) {
|
||||
if (metrics.fcp > 3000) score -= 10;
|
||||
else if (metrics.fcp > 1800) score -= 5;
|
||||
}
|
||||
|
||||
// 认证检查评分(权重 10%)
|
||||
if (metrics.authCheck) {
|
||||
if (metrics.authCheck > 500) score -= 10;
|
||||
else if (metrics.authCheck > 300) score -= 5;
|
||||
}
|
||||
|
||||
return Math.max(0, Math.min(100, score));
|
||||
};
|
||||
|
||||
export default posthog;
|
||||
|
||||
Reference in New Issue
Block a user