open-prompt / src /components /analytics /web-vitals.tsx
anky2002's picture
feat: add Web Vitals reporting for Core Web Vitals monitoring
9d380cf verified
"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"
}
}