Steve Kinney

Production Performance Monitoring

Your React app passes all performance tests in development, achieves perfect Lighthouse scores in CI, and feels blazing fast on your MacBook Pro. Then you deploy to production and discover the harsh reality: your users are on slow devices, unreliable networks, and older browsers. That 2-second page load becomes 8 seconds on a Moto G4 over 3G in India. Without production performance monitoring, you’re flying blind.

Real User Monitoring (RUM) bridges the gap between synthetic testing and actual user experience. It captures performance data from real users with real devices, real networks, and real usage patterns. This data reveals performance issues that synthetic tests miss: the shopping cart that’s slow only when it has 50+ items, the search that degrades with concurrent users, or the iOS bug that only affects Safari 14.1.

Understanding Real User Monitoring (RUM)

RUM collects performance metrics from actual user sessions, providing insights into how your React app performs in the wild:

// Real User Monitoring architecture
interface RUMData {
  // Core Web Vitals - Google's user experience metrics
  vitals: {
    LCP: number; // Largest Contentful Paint
    FID: number; // First Input Delay
    CLS: number; // Cumulative Layout Shift
    FCP: number; // First Contentful Paint
    TTFB: number; // Time to First Byte
  };

  // React-specific metrics
  react: {
    renderTime: number; // Component render duration
    hydrationTime: number; // SSR hydration duration
    bundleSize: number; // JavaScript bundle size
    componentCount: number; // Number of components rendered
    reRenderCount: number; // Unnecessary re-renders
  };

  // User context
  context: {
    userId?: string;
    sessionId: string;
    userAgent: string;
    viewport: { width: number; height: number };
    connection: 'slow-2g' | '2g' | '3g' | '4g' | 'wifi';
    deviceMemory?: number;
    hardwareConcurrency?: number;
  };

  // Page context
  page: {
    url: string;
    referrer: string;
    loadType: 'navigation' | 'reload' | 'back-forward';
    route: string;
    timestamp: number;
  };
}

Unlike synthetic monitoring that runs in controlled environments, RUM captures the chaos of real-world usage: users on slow networks, old devices, with browser extensions, antivirus software, and dozens of other tabs open.

Implementing Core Web Vitals Monitoring

Web Vitals Library Integration

// rum/web-vitals-collector.ts
import { getLCP, getFID, getCLS, getFCP, getTTFB } from 'web-vitals';

interface WebVitalMetric {
  name: string;
  value: number;
  delta: number;
  id: string;
  rating: 'good' | 'needs-improvement' | 'poor';
  entries: PerformanceEntry[];
}

class WebVitalsCollector {
  private metrics: Map<string, WebVitalMetric> = new Map();
  private isEnabled = true;

  constructor(
    private config: {
      sampleRate?: number;
      endpoint?: string;
      debug?: boolean;
    } = {},
  ) {
    // Sample only a percentage of users to manage data volume
    this.isEnabled = Math.random() < (config.sampleRate || 0.1);

    if (this.isEnabled) {
      this.setupCollection();
    }
  }

  private setupCollection(): void {
    // Largest Contentful Paint
    getLCP((metric) => {
      this.collectMetric(metric);

      if (this.config.debug) {
        this.highlightLCPElement(metric);
      }
    });

    // First Input Delay
    getFID((metric) => {
      this.collectMetric(metric);
    });

    // Cumulative Layout Shift
    getCLS((metric) => {
      this.collectMetric(metric);

      if (this.config.debug) {
        this.logLayoutShifts(metric);
      }
    });

    // First Contentful Paint
    getFCP((metric) => {
      this.collectMetric(metric);
    });

    // Time to First Byte
    getTTFB((metric) => {
      this.collectMetric(metric);
    });

    // Send metrics when page is unloaded
    this.setupBeaconSending();
  }

  private collectMetric(metric: WebVitalMetric): void {
    this.metrics.set(metric.name, metric);

    if (this.config.debug) {
      console.log(`📊 ${metric.name}: ${metric.value}ms (${metric.rating})`);
    }

    // Send individual metric immediately for critical issues
    if (metric.rating === 'poor' && this.config.endpoint) {
      this.sendMetric(metric);
    }
  }

  private highlightLCPElement(metric: WebVitalMetric): void {
    // Highlight the LCP element in development
    const lcpEntry = metric.entries[metric.entries.length - 1] as any;
    if (lcpEntry?.element) {
      lcpEntry.element.style.outline = '3px solid red';
      console.log('🎯 LCP Element:', lcpEntry.element);
    }
  }

  private logLayoutShifts(metric: WebVitalMetric): void {
    // Log layout shift sources
    metric.entries.forEach((entry: any) => {
      if (entry.sources) {
        entry.sources.forEach((source: any) => {
          console.log('📏 Layout shift source:', source.node);
        });
      }
    });
  }

  private setupBeaconSending(): void {
    // Send data when user leaves the page
    const sendBeacon = () => {
      this.sendAllMetrics();
    };

    // Multiple events to catch page unload
    addEventListener('beforeunload', sendBeacon);
    addEventListener('pagehide', sendBeacon);
    addEventListener('visibilitychange', () => {
      if (document.visibilityState === 'hidden') {
        sendBeacon();
      }
    });
  }

  private async sendMetric(metric: WebVitalMetric): Promise<void> {
    if (!this.config.endpoint) return;

    const payload = {
      metric,
      context: this.getContext(),
      timestamp: Date.now(),
    };

    try {
      // Use sendBeacon for reliability
      if (navigator.sendBeacon) {
        navigator.sendBeacon(this.config.endpoint, JSON.stringify(payload));
      } else {
        // Fallback to fetch with keepalive
        fetch(this.config.endpoint, {
          method: 'POST',
          body: JSON.stringify(payload),
          headers: { 'Content-Type': 'application/json' },
          keepalive: true,
        }).catch(() => {
          // Fail silently to avoid affecting user experience
        });
      }
    } catch (error) {
      // Fail silently
    }
  }

  private sendAllMetrics(): void {
    if (!this.config.endpoint || this.metrics.size === 0) return;

    const payload = {
      metrics: Object.fromEntries(this.metrics),
      context: this.getContext(),
      timestamp: Date.now(),
    };

    if (navigator.sendBeacon) {
      navigator.sendBeacon(this.config.endpoint, JSON.stringify(payload));
    }
  }

  private getContext(): any {
    return {
      url: location.href,
      userAgent: navigator.userAgent,
      viewport: {
        width: window.innerWidth,
        height: window.innerHeight,
      },
      connection: (navigator as any).connection?.effectiveType,
      deviceMemory: (navigator as any).deviceMemory,
      hardwareConcurrency: navigator.hardwareConcurrency,
      timestamp: Date.now(),
    };
  }

  getMetrics(): Map<string, WebVitalMetric> {
    return new Map(this.metrics);
  }
}

// Initialize Web Vitals collection
const webVitalsCollector = new WebVitalsCollector({
  sampleRate: 0.1, // Monitor 10% of users
  endpoint: '/api/vitals',
  debug: process.env.NODE_ENV === 'development',
});

React-Specific Performance Monitoring

// rum/react-performance-monitor.ts
import { Profiler, ProfilerOnRenderCallback } from 'react';

interface ReactPerformanceData {
  componentName: string;
  phase: 'mount' | 'update';
  actualDuration: number;
  baseDuration: number;
  startTime: number;
  commitTime: number;
  interactions: Set<any>;
}

class ReactPerformanceMonitor {
  private renderData: ReactPerformanceData[] = [];
  private slowRenders: ReactPerformanceData[] = [];
  private componentCounts = new Map<string, number>();

  constructor(
    private config: {
      slowRenderThreshold?: number;
      maxDataPoints?: number;
      reportingEndpoint?: string;
    } = {},
  ) {
    this.config.slowRenderThreshold = config.slowRenderThreshold || 16; // 60fps
    this.config.maxDataPoints = config.maxDataPoints || 1000;
  }

  createProfilerCallback(componentName: string): ProfilerOnRenderCallback {
    return (id, phase, actualDuration, baseDuration, startTime, commitTime, interactions) => {
      const renderData: ReactPerformanceData = {
        componentName,
        phase,
        actualDuration,
        baseDuration,
        startTime,
        commitTime,
        interactions,
      };

      // Track render data
      this.renderData.push(renderData);

      // Track component render counts
      const currentCount = this.componentCounts.get(componentName) || 0;
      this.componentCounts.set(componentName, currentCount + 1);

      // Flag slow renders
      if (actualDuration > this.config.slowRenderThreshold!) {
        this.slowRenders.push(renderData);

        if (process.env.NODE_ENV === 'development') {
          console.warn(
            `🐌 Slow render detected in ${componentName}: ${actualDuration.toFixed(2)}ms`,
          );
        }
      }

      // Limit data to prevent memory leaks
      if (this.renderData.length > this.config.maxDataPoints!) {
        this.renderData = this.renderData.slice(-this.config.maxDataPoints! / 2);
      }

      if (this.slowRenders.length > 100) {
        this.slowRenders = this.slowRenders.slice(-50);
      }
    };
  }

  generateReport(): {
    totalRenders: number;
    slowRenders: number;
    averageRenderTime: number;
    slowestComponents: Array<{ component: string; avgTime: number; count: number }>;
    recommendations: string[];
  } {
    const totalRenders = this.renderData.length;
    const slowRenderCount = this.slowRenders.length;

    const avgRenderTime =
      totalRenders > 0
        ? this.renderData.reduce((sum, data) => sum + data.actualDuration, 0) / totalRenders
        : 0;

    // Analyze slowest components
    const componentStats = new Map<string, { totalTime: number; count: number }>();

    this.renderData.forEach((data) => {
      const existing = componentStats.get(data.componentName) || { totalTime: 0, count: 0 };
      componentStats.set(data.componentName, {
        totalTime: existing.totalTime + data.actualDuration,
        count: existing.count + 1,
      });
    });

    const slowestComponents = Array.from(componentStats.entries())
      .map(([component, stats]) => ({
        component,
        avgTime: stats.totalTime / stats.count,
        count: stats.count,
      }))
      .sort((a, b) => b.avgTime - a.avgTime)
      .slice(0, 10);

    // Generate recommendations
    const recommendations: string[] = [];

    if (slowRenderCount / totalRenders > 0.1) {
      recommendations.push(
        'High percentage of slow renders detected - review component optimization',
      );
    }

    if (slowestComponents.length > 0 && slowestComponents[0].avgTime > 50) {
      recommendations.push(
        `Consider optimizing ${slowestComponents[0].component} - average render time: ${slowestComponents[0].avgTime.toFixed(2)}ms`,
      );
    }

    const frequentReRenders = Array.from(this.componentCounts.entries())
      .filter(([, count]) => count > 100)
      .map(([component]) => component);

    if (frequentReRenders.length > 0) {
      recommendations.push(
        `These components re-render frequently: ${frequentReRenders.join(', ')}`,
      );
    }

    return {
      totalRenders,
      slowRenders: slowRenderCount,
      averageRenderTime: avgRenderTime,
      slowestComponents,
      recommendations,
    };
  }

  async sendReport(): Promise<void> {
    if (!this.config.reportingEndpoint) return;

    const report = this.generateReport();
    const payload = {
      ...report,
      context: {
        url: location.href,
        userAgent: navigator.userAgent,
        timestamp: Date.now(),
      },
    };

    try {
      if (navigator.sendBeacon) {
        navigator.sendBeacon(this.config.reportingEndpoint, JSON.stringify(payload));
      } else {
        fetch(this.config.reportingEndpoint, {
          method: 'POST',
          body: JSON.stringify(payload),
          headers: { 'Content-Type': 'application/json' },
          keepalive: true,
        }).catch(() => {}); // Fail silently
      }
    } catch (error) {
      // Fail silently
    }
  }
}

// Global monitor instance
const reactPerformanceMonitor = new ReactPerformanceMonitor({
  slowRenderThreshold: 16,
  reportingEndpoint: '/api/react-performance',
});

// HOC for automatic profiling
function withPerformanceMonitoring<T extends {}>(
  Component: React.ComponentType<T>,
  componentName?: string,
): React.ComponentType<T> {
  const name = componentName || Component.displayName || Component.name || 'UnknownComponent';

  return function MonitoredComponent(props: T) {
    return (
      <Profiler id={name} onRender={reactPerformanceMonitor.createProfilerCallback(name)}>
        <Component {...props} />
      </Profiler>
    );
  };
}

// Hook for component-level monitoring
function usePerformanceMonitoring(componentName: string) {
  return {
    onRender: reactPerformanceMonitor.createProfilerCallback(componentName),
    generateReport: () => reactPerformanceMonitor.generateReport(),
  };
}

Error and Performance Correlation

// rum/error-performance-correlator.ts
interface PerformanceError {
  error: Error;
  context: {
    url: string;
    userAgent: string;
    timestamp: number;
    performanceData: {
      renderTime?: number;
      memoryUsage?: number;
      networkSpeed?: string;
    };
  };
}

class ErrorPerformanceCorrelator {
  private errors: PerformanceError[] = [];
  private performanceThresholds = {
    slowRender: 100, // ms
    highMemory: 100 * 1024 * 1024, // 100MB
    slowNetwork: ['slow-2g', '2g'],
  };

  captureError(error: Error, performanceData?: any): void {
    const errorData: PerformanceError = {
      error,
      context: {
        url: location.href,
        userAgent: navigator.userAgent,
        timestamp: Date.now(),
        performanceData: {
          renderTime: performanceData?.renderTime,
          memoryUsage: (performance as any).memory?.usedJSHeapSize,
          networkSpeed: (navigator as any).connection?.effectiveType,
        },
      },
    };

    this.errors.push(errorData);

    // Analyze correlation
    this.analyzeCorrelation(errorData);

    // Send to error tracking service
    this.sendErrorReport(errorData);
  }

  private analyzeCorrelation(errorData: PerformanceError): void {
    const { performanceData } = errorData.context;
    const correlations: string[] = [];

    // Check for slow render correlation
    if (
      performanceData.renderTime &&
      performanceData.renderTime > this.performanceThresholds.slowRender
    ) {
      correlations.push(`slow-render-${performanceData.renderTime}ms`);
    }

    // Check for high memory correlation
    if (
      performanceData.memoryUsage &&
      performanceData.memoryUsage > this.performanceThresholds.highMemory
    ) {
      correlations.push(`high-memory-${Math.round(performanceData.memoryUsage / 1024 / 1024)}MB`);
    }

    // Check for slow network correlation
    if (
      performanceData.networkSpeed &&
      this.performanceThresholds.slowNetwork.includes(performanceData.networkSpeed)
    ) {
      correlations.push(`slow-network-${performanceData.networkSpeed}`);
    }

    if (correlations.length > 0) {
      console.warn(`🔗 Error correlated with performance issues: ${correlations.join(', ')}`);

      // Add correlation data to error
      (errorData as any).performanceCorrelations = correlations;
    }
  }

  private async sendErrorReport(errorData: PerformanceError): Promise<void> {
    // Send to error tracking service (Sentry, Bugsnag, etc.)
    try {
      await fetch('/api/errors', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(errorData),
        keepalive: true,
      });
    } catch (error) {
      // Fail silently
    }
  }

  generateCorrelationReport(): {
    totalErrors: number;
    performanceCorrelatedErrors: number;
    topCorrelations: Array<{ correlation: string; count: number }>;
  } {
    const performanceCorrelatedErrors = this.errors.filter(
      (error) => (error as any).performanceCorrelations?.length > 0,
    );

    // Count correlation patterns
    const correlationCounts = new Map<string, number>();

    performanceCorrelatedErrors.forEach((error) => {
      const correlations = (error as any).performanceCorrelations || [];
      correlations.forEach((correlation: string) => {
        const count = correlationCounts.get(correlation) || 0;
        correlationCounts.set(correlation, count + 1);
      });
    });

    const topCorrelations = Array.from(correlationCounts.entries())
      .map(([correlation, count]) => ({ correlation, count }))
      .sort((a, b) => b.count - a.count)
      .slice(0, 10);

    return {
      totalErrors: this.errors.length,
      performanceCorrelatedErrors: performanceCorrelatedErrors.length,
      topCorrelations,
    };
  }
}

// Global error correlation instance
const errorCorrelator = new ErrorPerformanceCorrelator();

// React Error Boundary with performance correlation
class PerformanceAwareErrorBoundary extends React.Component<
  { children: React.ReactNode },
  { hasError: boolean }
> {
  constructor(props: any) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error: Error): { hasError: boolean } {
    return { hasError: true };
  }

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    // Capture performance data at time of error
    const performanceData = {
      renderTime: performance.now(), // Time since navigation start
      memoryUsage: (performance as any).memory?.usedJSHeapSize,
      networkSpeed: (navigator as any).connection?.effectiveType,
    };

    // Send error with performance correlation
    errorCorrelator.captureError(error, performanceData);

    console.error('React Error Boundary caught error:', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return (
        <div className="error-fallback">
          <h2>Something went wrong</h2>
          <p>Please refresh the page to continue.</p>
          <button onClick={() => window.location.reload()}>Refresh Page</button>
        </div>
      );
    }

    return this.props.children;
  }
}

Performance Alerting System

// rum/performance-alerting.ts
interface PerformanceAlert {
  type: 'web-vital' | 'react-performance' | 'error-spike' | 'memory-leak';
  severity: 'low' | 'medium' | 'high' | 'critical';
  message: string;
  data: any;
  timestamp: number;
}

class PerformanceAlertingSystem {
  private alerts: PerformanceAlert[] = [];
  private thresholds = {
    lcp: { good: 2500, poor: 4000 },
    fid: { good: 100, poor: 300 },
    cls: { good: 0.1, poor: 0.25 },
    renderTime: { good: 16, poor: 100 },
    memoryUsage: { good: 50 * 1024 * 1024, poor: 200 * 1024 * 1024 },
  };

  private alertHandlers: Array<(alert: PerformanceAlert) => void> = [];

  onAlert(handler: (alert: PerformanceAlert) => void): void {
    this.alertHandlers.push(handler);
  }

  checkWebVital(name: string, value: number): void {
    const threshold = this.thresholds[name as keyof typeof this.thresholds];
    if (!threshold) return;

    let severity: PerformanceAlert['severity'] = 'low';

    if (name === 'cls') {
      // CLS uses different scale
      if (value > threshold.poor) severity = 'critical';
      else if (value > threshold.good) severity = 'medium';
    } else {
      if (value > threshold.poor) severity = 'critical';
      else if (value > threshold.good) severity = 'medium';
    }

    if (severity !== 'low') {
      this.createAlert({
        type: 'web-vital',
        severity,
        message: `${name.toUpperCase()} is ${severity}: ${value}${name === 'cls' ? '' : 'ms'}`,
        data: { metric: name, value, threshold },
        timestamp: Date.now(),
      });
    }
  }

  checkReactPerformance(componentName: string, renderTime: number): void {
    let severity: PerformanceAlert['severity'] = 'low';

    if (renderTime > this.thresholds.renderTime.poor) {
      severity = 'high';
    } else if (renderTime > this.thresholds.renderTime.good) {
      severity = 'medium';
    }

    if (severity !== 'low') {
      this.createAlert({
        type: 'react-performance',
        severity,
        message: `Slow render in ${componentName}: ${renderTime.toFixed(2)}ms`,
        data: { component: componentName, renderTime },
        timestamp: Date.now(),
      });
    }
  }

  checkMemoryUsage(usage: number): void {
    let severity: PerformanceAlert['severity'] = 'low';

    if (usage > this.thresholds.memoryUsage.poor) {
      severity = 'critical';
    } else if (usage > this.thresholds.memoryUsage.good) {
      severity = 'medium';
    }

    if (severity !== 'low') {
      this.createAlert({
        type: 'memory-leak',
        severity,
        message: `High memory usage: ${(usage / 1024 / 1024).toFixed(2)}MB`,
        data: { memoryUsage: usage },
        timestamp: Date.now(),
      });
    }
  }

  private createAlert(alert: PerformanceAlert): void {
    this.alerts.push(alert);

    // Limit stored alerts
    if (this.alerts.length > 100) {
      this.alerts = this.alerts.slice(-50);
    }

    // Notify handlers
    this.alertHandlers.forEach((handler) => {
      try {
        handler(alert);
      } catch (error) {
        console.error('Alert handler error:', error);
      }
    });

    // Log critical alerts
    if (alert.severity === 'critical') {
      console.error('🚨 Critical Performance Alert:', alert.message);
    }
  }

  getAlerts(severity?: PerformanceAlert['severity']): PerformanceAlert[] {
    return severity ? this.alerts.filter((alert) => alert.severity === severity) : [...this.alerts];
  }

  generateAlertReport(): {
    totalAlerts: number;
    alertsBySeverity: Record<string, number>;
    recentCriticalAlerts: PerformanceAlert[];
  } {
    const alertsBySeverity = this.alerts.reduce(
      (acc, alert) => {
        acc[alert.severity] = (acc[alert.severity] || 0) + 1;
        return acc;
      },
      {} as Record<string, number>,
    );

    const oneDayAgo = Date.now() - 24 * 60 * 60 * 1000;
    const recentCriticalAlerts = this.alerts
      .filter((alert) => alert.severity === 'critical' && alert.timestamp > oneDayAgo)
      .slice(-10);

    return {
      totalAlerts: this.alerts.length,
      alertsBySeverity,
      recentCriticalAlerts,
    };
  }
}

// Global alerting system
const performanceAlerting = new PerformanceAlertingSystem();

// Setup alert handlers
performanceAlerting.onAlert((alert) => {
  // Send critical alerts to monitoring service immediately
  if (alert.severity === 'critical') {
    fetch('/api/alerts', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(alert),
      keepalive: true,
    }).catch(() => {}); // Fail silently
  }
});

// Setup visual alerts for development
if (process.env.NODE_ENV === 'development') {
  performanceAlerting.onAlert((alert) => {
    if (alert.severity === 'high' || alert.severity === 'critical') {
      // Show toast notification in development
      const toast = document.createElement('div');
      toast.style.cssText = `
        position: fixed;
        top: 20px;
        right: 20px;
        background: ${alert.severity === 'critical' ? '#d32f2f' : '#f57c00'};
        color: white;
        padding: 12px 16px;
        border-radius: 4px;
        z-index: 10000;
        font-family: monospace;
        font-size: 12px;
        max-width: 300px;
      `;
      toast.textContent = alert.message;

      document.body.appendChild(toast);

      setTimeout(() => {
        document.body.removeChild(toast);
      }, 5000);
    }
  });
}

Analytics Integration

// rum/analytics-integration.ts
interface AnalyticsEvent {
  category: 'Performance';
  action: string;
  label?: string;
  value?: number;
  customDimensions?: Record<string, string | number>;
}

class PerformanceAnalytics {
  private queue: AnalyticsEvent[] = [];
  private isOnline = navigator.onLine;

  constructor(
    private config: {
      googleAnalyticsId?: string;
      customEndpoint?: string;
      batchSize?: number;
    } = {},
  ) {
    // Monitor online status
    addEventListener('online', () => {
      this.isOnline = true;
      this.flushQueue();
    });

    addEventListener('offline', () => {
      this.isOnline = false;
    });

    // Flush queue periodically
    setInterval(() => this.flushQueue(), 30000); // Every 30 seconds
  }

  trackWebVital(name: string, value: number, rating: string): void {
    this.track({
      category: 'Performance',
      action: 'Web Vital',
      label: name,
      value: Math.round(value),
      customDimensions: {
        rating,
        url: location.pathname,
        device: this.getDeviceType(),
        connection: this.getConnectionType(),
      },
    });
  }

  trackReactRender(componentName: string, duration: number, phase: string): void {
    // Only track slow renders to reduce noise
    if (duration > 16) {
      this.track({
        category: 'Performance',
        action: 'Slow React Render',
        label: componentName,
        value: Math.round(duration),
        customDimensions: {
          phase,
          url: location.pathname,
        },
      });
    }
  }

  trackBundle(bundleName: string, size: number, loadTime: number): void {
    this.track({
      category: 'Performance',
      action: 'Bundle Load',
      label: bundleName,
      value: Math.round(loadTime),
      customDimensions: {
        size: Math.round(size / 1024), // KB
        url: location.pathname,
      },
    });
  }

  trackError(error: string, performanceContext: any): void {
    this.track({
      category: 'Performance',
      action: 'Performance Related Error',
      label: error,
      customDimensions: {
        memoryUsage: Math.round((performanceContext.memoryUsage || 0) / 1024 / 1024), // MB
        renderTime: performanceContext.renderTime,
        url: location.pathname,
      },
    });
  }

  private track(event: AnalyticsEvent): void {
    this.queue.push(event);

    // Send immediately if online and queue is not too large
    if (this.isOnline && this.queue.length >= (this.config.batchSize || 10)) {
      this.flushQueue();
    }
  }

  private async flushQueue(): Promise<void> {
    if (!this.isOnline || this.queue.length === 0) return;

    const events = this.queue.splice(0, this.config.batchSize || 10);

    // Send to Google Analytics
    if (this.config.googleAnalyticsId && typeof gtag !== 'undefined') {
      events.forEach((event) => {
        gtag('event', event.action, {
          event_category: event.category,
          event_label: event.label,
          value: event.value,
          custom_map: event.customDimensions,
        });
      });
    }

    // Send to custom endpoint
    if (this.config.customEndpoint) {
      try {
        const response = await fetch(this.config.customEndpoint, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ events }),
          keepalive: true,
        });

        if (!response.ok) {
          // Re-queue events on failure
          this.queue.unshift(...events);
        }
      } catch (error) {
        // Re-queue events on failure
        this.queue.unshift(...events);
      }
    }
  }

  private getDeviceType(): string {
    const width = window.innerWidth;
    if (width < 768) return 'mobile';
    if (width < 1024) return 'tablet';
    return 'desktop';
  }

  private getConnectionType(): string {
    return (navigator as any).connection?.effectiveType || 'unknown';
  }
}

// Global analytics instance
const performanceAnalytics = new PerformanceAnalytics({
  googleAnalyticsId: 'GA_MEASUREMENT_ID',
  customEndpoint: '/api/performance-analytics',
  batchSize: 5,
});

Dashboard and Reporting

// rum/performance-dashboard.ts
interface DashboardData {
  webVitals: {
    lcp: { p50: number; p75: number; p95: number };
    fid: { p50: number; p75: number; p95: number };
    cls: { p50: number; p75: number; p95: number };
  };
  reactMetrics: {
    averageRenderTime: number;
    slowRenders: number;
    topSlowComponents: Array<{ name: string; avgTime: number }>;
  };
  errors: {
    total: number;
    performanceRelated: number;
    topErrors: Array<{ message: string; count: number }>;
  };
  trends: {
    performanceScore: number[];
    memoryUsage: number[];
    bundleSize: number[];
  };
}

class PerformanceDashboard {
  async fetchDashboardData(timeRange: '1h' | '24h' | '7d' | '30d' = '24h'): Promise<DashboardData> {
    try {
      const response = await fetch(`/api/performance-dashboard?range=${timeRange}`);
      return await response.json();
    } catch (error) {
      console.error('Failed to fetch dashboard data:', error);
      return this.getEmptyDashboardData();
    }
  }

  generatePerformanceScore(data: DashboardData): number {
    // Calculate overall performance score (0-100)
    const webVitalsScore = this.calculateWebVitalsScore(data.webVitals);
    const reactScore = this.calculateReactScore(data.reactMetrics);
    const errorScore = this.calculateErrorScore(data.errors);

    return Math.round(webVitalsScore * 0.5 + reactScore * 0.3 + errorScore * 0.2);
  }

  private calculateWebVitalsScore(vitals: DashboardData['webVitals']): number {
    // Score based on Google's thresholds
    const lcpScore = vitals.lcp.p75 <= 2500 ? 100 : vitals.lcp.p75 <= 4000 ? 50 : 0;
    const fidScore = vitals.fid.p75 <= 100 ? 100 : vitals.fid.p75 <= 300 ? 50 : 0;
    const clsScore = vitals.cls.p75 <= 0.1 ? 100 : vitals.cls.p75 <= 0.25 ? 50 : 0;

    return (lcpScore + fidScore + clsScore) / 3;
  }

  private calculateReactScore(reactMetrics: DashboardData['reactMetrics']): number {
    // Score based on React performance
    const avgRenderScore =
      reactMetrics.averageRenderTime <= 16 ? 100 : reactMetrics.averageRenderTime <= 50 ? 70 : 40;

    const slowRenderScore =
      reactMetrics.slowRenders === 0 ? 100 : reactMetrics.slowRenders < 10 ? 80 : 50;

    return (avgRenderScore + slowRenderScore) / 2;
  }

  private calculateErrorScore(errors: DashboardData['errors']): number {
    if (errors.total === 0) return 100;

    const errorRate = errors.performanceRelated / errors.total;

    if (errorRate < 0.01) return 100; // < 1%
    if (errorRate < 0.05) return 80; // < 5%
    if (errorRate < 0.1) return 60; // < 10%
    return 40;
  }

  generateRecommendations(data: DashboardData): string[] {
    const recommendations: string[] = [];

    // Web Vitals recommendations
    if (data.webVitals.lcp.p75 > 2500) {
      recommendations.push(
        'Optimize Largest Contentful Paint - consider image optimization, preloading critical resources',
      );
    }

    if (data.webVitals.cls.p75 > 0.1) {
      recommendations.push(
        'Reduce Cumulative Layout Shift - ensure images have dimensions, avoid dynamic content insertion',
      );
    }

    if (data.webVitals.fid.p75 > 100) {
      recommendations.push(
        'Improve First Input Delay - reduce JavaScript execution time, use code splitting',
      );
    }

    // React recommendations
    if (data.reactMetrics.averageRenderTime > 16) {
      recommendations.push(
        'Optimize React render performance - review component memoization and state management',
      );
    }

    if (data.reactMetrics.slowRenders > 50) {
      recommendations.push(
        `High number of slow renders (${data.reactMetrics.slowRenders}) - investigate top slow components`,
      );
    }

    // Error recommendations
    if (data.errors.performanceRelated / data.errors.total > 0.05) {
      recommendations.push(
        'High rate of performance-related errors - review error-performance correlations',
      );
    }

    return recommendations;
  }

  private getEmptyDashboardData(): DashboardData {
    return {
      webVitals: {
        lcp: { p50: 0, p75: 0, p95: 0 },
        fid: { p50: 0, p75: 0, p95: 0 },
        cls: { p50: 0, p75: 0, p95: 0 },
      },
      reactMetrics: {
        averageRenderTime: 0,
        slowRenders: 0,
        topSlowComponents: [],
      },
      errors: {
        total: 0,
        performanceRelated: 0,
        topErrors: [],
      },
      trends: {
        performanceScore: [],
        memoryUsage: [],
        bundleSize: [],
      },
    };
  }
}

// React component for performance dashboard
function PerformanceDashboardComponent() {
  const [data, setData] = useState<DashboardData | null>(null);
  const [loading, setLoading] = useState(true);
  const dashboard = useRef(new PerformanceDashboard());

  useEffect(() => {
    const loadData = async () => {
      setLoading(true);
      const dashboardData = await dashboard.current.fetchDashboardData('24h');
      setData(dashboardData);
      setLoading(false);
    };

    loadData();

    // Refresh every 5 minutes
    const interval = setInterval(loadData, 5 * 60 * 1000);
    return () => clearInterval(interval);
  }, []);

  if (loading) return <div>Loading performance data...</div>;
  if (!data) return <div>Failed to load performance data</div>;

  const performanceScore = dashboard.current.generatePerformanceScore(data);
  const recommendations = dashboard.current.generateRecommendations(data);

  return (
    <div className="performance-dashboard">
      <h2>Performance Dashboard</h2>

      <div className="performance-score">
        <h3>Overall Score: {performanceScore}/100</h3>
      </div>

      <div className="web-vitals">
        <h3>Core Web Vitals</h3>
        <div>LCP (p75): {data.webVitals.lcp.p75}ms</div>
        <div>FID (p75): {data.webVitals.fid.p75}ms</div>
        <div>CLS (p75): {data.webVitals.cls.p75}</div>
      </div>

      <div className="react-metrics">
        <h3>React Performance</h3>
        <div>Average Render Time: {data.reactMetrics.averageRenderTime.toFixed(2)}ms</div>
        <div>Slow Renders: {data.reactMetrics.slowRenders}</div>
      </div>

      <div className="recommendations">
        <h3>Recommendations</h3>
        <ul>
          {recommendations.map((rec, index) => (
            <li key={index}>{rec}</li>
          ))}
        </ul>
      </div>
    </div>
  );
}

Last modified on .