import { onCLS, onINP, onFCP, onLCP, onTTFB, type Metric } from 'web-vitals'; /** * Performance Monitoring Module * * Tracks Core Web Vitals metrics: * - CLS (Cumulative Layout Shift): Measures visual stability * - INP (Interaction to Next Paint): Measures interactivity (replaces FID in v5) * - FCP (First Contentful Paint): Measures perceived load speed * - LCP (Largest Contentful Paint): Measures loading performance * - TTFB (Time to First Byte): Measures server response time * * Sends metrics to analytics (Google Analytics if available) */ interface PerformanceMetric { name: string; value: number; id: string; delta: number; rating: 'good' | 'needs-improvement' | 'poor'; } /** * Send metric to analytics service */ const sendToAnalytics = (metric: Metric) => { const body: PerformanceMetric = { name: metric.name, value: Math.round(metric.value), id: metric.id, delta: metric.delta, rating: metric.rating, }; // Send to Google Analytics if available if (typeof window !== 'undefined' && (window as any).gtag) { (window as any).gtag('event', metric.name, { event_category: 'Web Vitals', event_label: metric.id, value: Math.round(metric.value), metric_id: metric.id, metric_value: metric.value, metric_delta: metric.delta, metric_rating: metric.rating, non_interaction: true, }); } // Log to console in development if (process.env.NODE_ENV === 'development') { console.log('[Performance]', body); } // Send to custom analytics endpoint if needed if (process.env.NEXT_PUBLIC_ANALYTICS_ENDPOINT) { const analyticsEndpoint = process.env.NEXT_PUBLIC_ANALYTICS_ENDPOINT; // Use navigator.sendBeacon for reliable analytics even during page unload if (navigator.sendBeacon) { navigator.sendBeacon( analyticsEndpoint, JSON.stringify(body) ); } else { // Fallback to fetch fetch(analyticsEndpoint, { method: 'POST', body: JSON.stringify(body), headers: { 'Content-Type': 'application/json', }, keepalive: true, }).catch((error) => { console.error('Failed to send analytics:', error); }); } } }; /** * Initialize performance monitoring * Call this function once when the app loads */ export const initPerformanceMonitoring = () => { if (typeof window === 'undefined') { return; } try { // Track Cumulative Layout Shift (CLS) // Good: < 0.1, Needs Improvement: < 0.25, Poor: >= 0.25 onCLS(sendToAnalytics); // Track Interaction to Next Paint (INP) - replaces FID in web-vitals v5 // Good: < 200ms, Needs Improvement: < 500ms, Poor: >= 500ms onINP(sendToAnalytics); // Track First Contentful Paint (FCP) // Good: < 1.8s, Needs Improvement: < 3s, Poor: >= 3s onFCP(sendToAnalytics); // Track Largest Contentful Paint (LCP) // Good: < 2.5s, Needs Improvement: < 4s, Poor: >= 4s onLCP(sendToAnalytics); // Track Time to First Byte (TTFB) // Good: < 800ms, Needs Improvement: < 1800ms, Poor: >= 1800ms onTTFB(sendToAnalytics); console.log('[Performance] Monitoring initialized'); } catch (error) { console.error('[Performance] Failed to initialize monitoring:', error); } }; /** * Report custom performance metrics */ export const reportCustomMetric = (name: string, value: number, metadata?: Record) => { if (typeof window !== 'undefined' && (window as any).gtag) { (window as any).gtag('event', name, { event_category: 'Custom Metrics', value: Math.round(value), ...metadata, }); } if (process.env.NODE_ENV === 'development') { console.log('[Performance Custom Metric]', { name, value, metadata }); } }; /** * Measure and report component render time */ export const measureComponentRender = (componentName: string) => { if (typeof window === 'undefined' || !window.performance) { return () => {}; } const startMark = `${componentName}-render-start`; const endMark = `${componentName}-render-end`; const measureName = `${componentName}-render`; performance.mark(startMark); return () => { performance.mark(endMark); performance.measure(measureName, startMark, endMark); const measure = performance.getEntriesByName(measureName)[0]; if (measure) { reportCustomMetric(`component_render_${componentName}`, measure.duration, { component: componentName, }); } // Clean up marks and measures performance.clearMarks(startMark); performance.clearMarks(endMark); performance.clearMeasures(measureName); }; }; /** * Track page load time */ export const trackPageLoad = (pageName: string) => { if (typeof window === 'undefined' || !window.performance) { return; } // Wait for load event window.addEventListener('load', () => { setTimeout(() => { const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming; if (navigation) { reportCustomMetric(`page_load_${pageName}`, navigation.loadEventEnd - navigation.fetchStart, { page: pageName, dom_content_loaded: navigation.domContentLoadedEventEnd - navigation.fetchStart, dom_interactive: navigation.domInteractive - navigation.fetchStart, }); } }, 0); }); }; /** * Monitor long tasks (tasks that block the main thread for > 50ms) */ export const monitorLongTasks = () => { if (typeof window === 'undefined' || !(window as any).PerformanceObserver) { return; } try { const observer = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { reportCustomMetric('long_task', entry.duration, { start_time: entry.startTime, duration: entry.duration, }); } }); observer.observe({ entryTypes: ['longtask'] }); } catch (error) { console.error('[Performance] Failed to monitor long tasks:', error); } }; /** * Track resource loading times */ export const trackResourceTiming = () => { if (typeof window === 'undefined' || !window.performance) { return; } const resources = performance.getEntriesByType('resource') as PerformanceResourceTiming[]; const slowResources = resources.filter((resource) => resource.duration > 1000); slowResources.forEach((resource) => { reportCustomMetric('slow_resource', resource.duration, { url: resource.name, type: resource.initiatorType, size: resource.transferSize, }); }); };