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:
zdl
2025-11-25 16:36:18 +08:00
parent 6679d99cf9
commit cb46971e0e
5 changed files with 500 additions and 0 deletions

View 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;