"use client" import { useEffect } from "react" /** * Reports Core Web Vitals (LCP, FID, CLS, FCP, TTFB) to console in development * and can be extended to send to analytics in production. * * Uses the web-vitals library pattern via PerformanceObserver. */ export function WebVitals() { useEffect(() => { if (typeof window === "undefined") return // Only report in development or if analytics endpoint is configured const shouldReport = process.env.NODE_ENV === "development" || !!process.env.NEXT_PUBLIC_ANALYTICS_URL if (!shouldReport) return // Use Performance Observer for CLS, LCP, FID try { // Largest Contentful Paint const lcpObserver = new PerformanceObserver((list) => { const entries = list.getEntries() const lastEntry = entries[entries.length - 1] if (lastEntry) { reportMetric("LCP", lastEntry.startTime) } }) lcpObserver.observe({ type: "largest-contentful-paint", buffered: true }) // First Input Delay const fidObserver = new PerformanceObserver((list) => { const entries = list.getEntries() entries.forEach((entry) => { const fidEntry = entry as PerformanceEventTiming reportMetric("FID", fidEntry.processingStart - fidEntry.startTime) }) }) fidObserver.observe({ type: "first-input", buffered: true }) // Cumulative Layout Shift let clsValue = 0 const clsObserver = new PerformanceObserver((list) => { const entries = list.getEntries() entries.forEach((entry) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any if (!(entry as any).hadRecentInput) { // eslint-disable-next-line @typescript-eslint/no-explicit-any clsValue += (entry as any).value } }) reportMetric("CLS", clsValue) }) clsObserver.observe({ type: "layout-shift", buffered: true }) return () => { lcpObserver.disconnect() fidObserver.disconnect() clsObserver.disconnect() } } catch { // PerformanceObserver not supported } }, []) return null } function reportMetric(name: string, value: number) { if (process.env.NODE_ENV === "development") { const color = getMetricColor(name, value) console.log( `%c[Web Vital] ${name}: ${value.toFixed(2)}ms`, `color: ${color}; font-weight: bold;` ) } // Send to analytics endpoint if configured const analyticsUrl = process.env.NEXT_PUBLIC_ANALYTICS_URL if (analyticsUrl) { const body = JSON.stringify({ name, value, page: window.location.pathname }) // Use sendBeacon for reliability during page unload if (navigator.sendBeacon) { navigator.sendBeacon(analyticsUrl, body) } else { fetch(analyticsUrl, { method: "POST", body, keepalive: true }).catch(() => {}) } } } function getMetricColor(name: string, value: number): string { switch (name) { case "LCP": return value <= 2500 ? "green" : value <= 4000 ? "orange" : "red" case "FID": return value <= 100 ? "green" : value <= 300 ? "orange" : "red" case "CLS": return value <= 0.1 ? "green" : value <= 0.25 ? "orange" : "red" default: return "blue" } }