import React, { useState, useEffect } from 'react'; import { Activity, TrendingUp, AlertCircle, CheckCircle } from 'lucide-react'; interface WebVitalMetric { name: string; value: number; rating: 'good' | 'needs-improvement' | 'poor'; timestamp: number; } interface PerformanceMetrics { lcp: WebVitalMetric[]; inp: WebVitalMetric[]; cls: WebVitalMetric[]; fcp: WebVitalMetric[]; ttfb: WebVitalMetric[]; } const THRESHOLDS = { LCP: { good: 2500, poor: 4000 }, INP: { good: 200, poor: 500 }, CLS: { good: 0.1, poor: 0.25 }, FCP: { good: 1800, poor: 3000 }, TTFB: { good: 800, poor: 1800 } }; export function PerformanceMonitor() { const [metrics, setMetrics] = useState({ lcp: [], inp: [], cls: [], fcp: [], ttfb: [] }); const [isVisible, setIsVisible] = useState(false); useEffect(() => { // Listen for performance metrics from webVitals const handlePerformanceData = (event: CustomEvent) => { const metric = event.detail as WebVitalMetric; setMetrics(prev => ({ ...prev, [metric.name.toLowerCase()]: [...(prev[metric.name.toLowerCase() as keyof PerformanceMetrics] || []), metric].slice(-10) })); }; window.addEventListener('web-vitals', handlePerformanceData as EventListener); return () => { window.removeEventListener('web-vitals', handlePerformanceData as EventListener); }; }, []); const getLatestMetric = (metricName: keyof PerformanceMetrics) => { const metricArray = metrics[metricName]; return metricArray[metricArray.length - 1]; }; const getRatingColor = (rating?: string) => { switch (rating) { case 'good': return 'text-green-500'; case 'needs-improvement': return 'text-yellow-500'; case 'poor': return 'text-red-500'; default: return 'text-zinc-400'; } }; const getRatingIcon = (rating?: string) => { switch (rating) { case 'good': return ; case 'needs-improvement': return ; case 'poor': return ; default: return null; } }; const formatValue = (name: string, value?: number) => { if (value === undefined) return '-'; if (name === 'CLS') return value.toFixed(3); return `${Math.round(value)}ms`; }; const calculateScore = () => { const latestLCP = getLatestMetric('lcp'); const latestINP = getLatestMetric('inp'); const latestCLS = getLatestMetric('cls'); if (!latestLCP || !latestINP || !latestCLS) return null; let score = 100; // LCP scoring if (latestLCP.value > THRESHOLDS.LCP.poor) score -= 33; else if (latestLCP.value > THRESHOLDS.LCP.good) score -= 16; // INP scoring if (latestINP.value > THRESHOLDS.INP.poor) score -= 33; else if (latestINP.value > THRESHOLDS.INP.good) score -= 16; // CLS scoring if (latestCLS.value > THRESHOLDS.CLS.poor) score -= 34; else if (latestCLS.value > THRESHOLDS.CLS.good) score -= 18; return Math.max(0, Math.round(score)); }; // Only show in development or with special flag if (process.env.NODE_ENV === 'production' && !window.location.search.includes('debug=true')) { return null; } return ( <> {/* Toggle Button */} {/* Performance Panel */} {isVisible && (

Core Web Vitals

{/* Overall Score */} {calculateScore() !== null && (
Performance Score = 90 ? 'text-green-500' : calculateScore()! >= 50 ? 'text-yellow-500' : 'text-red-500' }`}> {calculateScore()}
)} {/* Metrics */}
{[ { key: 'lcp', label: 'Largest Contentful Paint', threshold: THRESHOLDS.LCP }, { key: 'inp', label: 'Interaction to Next Paint', threshold: THRESHOLDS.INP }, { key: 'cls', label: 'Cumulative Layout Shift', threshold: THRESHOLDS.CLS }, { key: 'fcp', label: 'First Contentful Paint', threshold: THRESHOLDS.FCP }, { key: 'ttfb', label: 'Time to First Byte', threshold: THRESHOLDS.TTFB } ].map(({ key, label }) => { const latest = getLatestMetric(key as keyof PerformanceMetrics); return (
{label}
{key}
{getRatingIcon(latest?.rating)} {formatValue(key.toUpperCase(), latest?.value)}
); })}
{/* Info */}

Real user metrics • Updates live • Learn more

)} ); }