← Back to Blog

Web Performance Monitoring: The Complete Guide (2024)

performanceweb-vitalsmonitoringoptimizationuser-experience

Web Performance Monitoring: The Complete Guide (2024)

💡

Use ScreenScout's Performance Monitoring to track and optimize your website's performance metrics in real-time!

Understanding Web Performance Monitoring

Web performance monitoring is the practice of measuring, analyzing, and optimizing the speed and efficiency of web applications. It's a critical aspect of web development that directly impacts user experience, business metrics, and search engine rankings.

The Business Case for Performance

Let's look at some real-world examples of how performance impacts business:

  • Pinterest reduced their wait time by 40% and saw a 15% increase in SEO traffic
  • BBC found they lost 10% of users for every additional second their site took to load
  • Financial Times discovered that a 1-second delay in page load time led to a 4.9% drop in article views
  • Mobify found that each 100ms improvement in homepage load speed resulted in a 1.11% increase in conversion
💡

Studies show that 47% of users expect websites to load in under 2 seconds, and 40% will abandon a site that takes more than 3 seconds to load. This directly impacts your bottom line.

Key Performance Metrics Explained

Before diving into Core Web Vitals, it's essential to understand the fundamental metrics that affect web performance:

  1. Time to First Byte (TTFB)

    • What it measures: Time from request to first byte of response
    • Good TTFB: < 200ms
    • Affects: Initial rendering and perceived performance
    • Common issues: Server configuration, network latency, DNS lookup time
  2. First Contentful Paint (FCP)

    • What it measures: Time until first content is painted
    • Good FCP: < 1.8s
    • Affects: User perception of page load
    • Optimization tips: Minimize render-blocking resources, optimize server response
  3. Time to Interactive (TTI)

    • What it measures: Time until page becomes fully interactive
    • Good TTI: < 3.8s
    • Affects: User frustration, bounce rates
    • Common bottlenecks: Heavy JavaScript, third-party scripts

Core Web Vitals Deep Dive

Core Web Vitals are Google's initiative to provide unified guidance for quality signals that are essential to delivering a great user experience on the web.

Largest Contentful Paint (LCP)

LCP measures loading performance by timing how long it takes to render the largest content element in the viewport.

type LCPMetric = {
  value: number        // Time in milliseconds
  element: Element     // The largest element
  rating: 'good' | 'needs-improvement' | 'poor'
  threshold: {
    good: 2500,       // Under 2.5s
    poor: 4000        // Over 4s
  }
}

What Affects LCP?

  1. Server Response Time

    • Optimize server configuration
    • Use CDN for global reach
    • Implement caching strategies
  2. Resource Load Time

    • Optimize images and fonts
    • Minimize render-blocking resources
    • Implement resource prioritization
  3. Client-side Rendering

    • Consider server-side rendering for critical content
    • Implement progressive hydration
    • Use streaming server-side rendering
💭

Common elements that define LCP:

  • Images (including background images)
  • Video thumbnails
  • Text blocks
  • SVG elements

Focus your optimization efforts on these elements first.

First Input Delay (FID)

FID measures interactivity by timing how long it takes for the page to respond to the first user interaction.

type FIDMetric = {
  value: number
  rating: 'good' | 'needs-improvement' | 'poor'
  threshold: {
    good: 100,        // Under 100ms
    poor: 300         // Over 300ms
  }
}

Understanding FID

  1. What Causes Poor FID?

    • Long-running JavaScript
    • Large JavaScript bundles
    • Heavy third-party scripts
    • Complex component initialization
  2. How to Measure FID

    // Real-user FID measurement
    import { onFID } from 'web-vitals';
    
    onFID(({ value, rating }) => {
      console.log(`FID: ${value}ms (${rating})`);
      
      // Common causes of poor FID
      performance.getEntriesByType('longtask').forEach(task => {
        if (task.duration > 50) {
          console.warn(`Long task detected: ${task.duration}ms`);
        }
      });
    });
    
  3. Optimization Strategies

    • Break up long tasks
    • Optimize JavaScript execution
    • Implement code splitting
    • Use web workers for heavy computation

Cumulative Layout Shift (CLS)

CLS measures visual stability by calculating how much unexpected layout shift occurs during the entire lifespan of the page.

type CLSMetric = {
  value: number
  rating: 'good' | 'needs-improvement' | 'poor'
  threshold: {
    good: 0.1,        // Under 0.1
    poor: 0.25        // Over 0.25
  }
}

Understanding Layout Shifts

  1. What Causes Layout Shifts?

    • Images without dimensions
    • Dynamically injected content
    • Web fonts causing FOUT/FOIT
    • Dynamic ads and embeds
  2. How to Calculate CLS

    // Layout shift score calculation
    function calculateLayoutShiftScore(impact, distance) {
      const impactFraction = impact; // Area of viewport affected
      const distanceFraction = distance / viewportHeight;
      return impactFraction * distanceFraction;
    }
    
    // Monitor layout shifts
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (!entry.hadRecentInput) {
          const score = calculateLayoutShiftScore(
            entry.impactFraction,
            entry.value
          );
          console.log(`Layout shift detected: ${score}`);
        }
      }
    });
    
    observer.observe({ entryTypes: ['layout-shift'] });
    
  3. Prevention Strategies

    <!-- Always specify image dimensions -->
    <img 
      src="hero.jpg" 
      width="800" 
      height="400" 
      loading="lazy"
      style="aspect-ratio: 2/1"
    />
    
    <!-- Reserve space for dynamic content -->
    <div style="min-height: 400px">
      <!-- Dynamic content here -->
    </div>
    
    <!-- Preload critical fonts -->
    <link 
      rel="preload" 
      href="font.woff2" 
      as="font" 
      type="font/woff2" 
      crossorigin
    />
    

Optimization Strategies: A Deep Dive

Performance optimization is an iterative process that requires a systematic approach. Let's explore detailed strategies for different aspects of web performance.

1. Resource Optimization

Image Optimization Strategies

Images often account for the largest portion of a webpage's size. Here's a comprehensive approach to image optimization:

  1. Format Selection

    interface ImageFormat {
      format: 'jpeg' | 'png' | 'webp' | 'avif'
      useCase: string[]
      compression: 'lossy' | 'lossless'
      browserSupport: string[]
      typical_savings: string
    }
    
    const formatGuide: Record<string, ImageFormat> = {
      webp: {
        format: 'webp',
        useCase: ['photographs', 'complex images'],
        compression: 'lossy',
        browserSupport: ['Chrome', 'Firefox', 'Edge', 'Safari 14+'],
        typical_savings: '25-35% vs JPEG'
      },
      avif: {
        format: 'avif',
        useCase: ['photographs', 'complex images'],
        compression: 'lossy',
        browserSupport: ['Chrome', 'Firefox'],
        typical_savings: '40-50% vs JPEG'
      }
    }
    
  2. Responsive Images Implementation

    interface ImageBreakpoint {
      width: number
      height: number
      quality: number
    }
    
    class ResponsiveImageGenerator {
      private breakpoints: ImageBreakpoint[] = [
        { width: 320, height: 240, quality: 70 },
        { width: 640, height: 480, quality: 75 },
        { width: 1024, height: 768, quality: 80 },
        { width: 1920, height: 1080, quality: 85 }
      ]
      
      generateSrcSet(imagePath: string): string {
        return this.breakpoints
          .map(bp => {
            const url = this.generateImageUrl(imagePath, bp)
            return `${url} ${bp.width}w`
          })
          .join(', ')
      }
      
      generateSizes(customSizes?: string): string {
        return customSizes || this.breakpoints
          .map(bp => `(max-width: ${bp.width}px) ${bp.width}px`)
          .join(', ')
      }
      
      private generateImageUrl(
        path: string,
        breakpoint: ImageBreakpoint
      ): string {
        // Implementation for your image service
        return `/image-service?path=${path}&w=${breakpoint.width}&q=${breakpoint.quality}`
      }
    }
    
  3. Lazy Loading Implementation

    class LazyLoader {
      private observer: IntersectionObserver
      private loadedImages: Set<string> = new Set()
      
      constructor() {
        this.observer = new IntersectionObserver(
          this.handleIntersection.bind(this),
          {
            rootMargin: '50px 0px',
            threshold: 0.01
          }
        )
      }
      
      observe(element: Element) {
        if (element.getAttribute('data-src')) {
          this.observer.observe(element)
        }
      }
      
      private async handleIntersection(
        entries: IntersectionObserverEntry[]
      ) {
        for (const entry of entries) {
          if (entry.isIntersecting) {
            const element = entry.target as HTMLImageElement
            const src = element.getAttribute('data-src')
            
            if (src && !this.loadedImages.has(src)) {
              await this.loadImage(element, src)
              this.loadedImages.add(src)
              this.observer.unobserve(element)
            }
          }
        }
      }
      
      private loadImage(
        img: HTMLImageElement,
        src: string
      ): Promise<void> {
        return new Promise((resolve, reject) => {
          img.onload = () => {
            img.classList.add('loaded')
            resolve()
          }
          img.onerror = reject
          img.src = src
        })
      }
    }
    

2. JavaScript Performance Optimization

Code Splitting and Dynamic Imports

Modern JavaScript applications can quickly grow in size. Here's how to implement intelligent code splitting:

// Router-based code splitting
interface Route {
  path: string
  component: () => Promise<any>
  preload?: boolean
}

class RouterConfig {
  private routes: Route[] = [
    {
      path: '/',
      component: () => import('./pages/Home'),
      preload: true
    },
    {
      path: '/dashboard',
      component: () => import('./pages/Dashboard')
    },
    {
      path: '/settings',
      component: () => import('./pages/Settings')
    }
  ]
  
  constructor() {
    this.setupPreloading()
  }
  
  private setupPreloading() {
    // Preload critical routes
    this.routes
      .filter(route => route.preload)
      .forEach(route => {
        const link = document.createElement('link')
        link.rel = 'modulepreload'
        link.href = this.getModulePath(route.path)
        document.head.appendChild(link)
      })
    
    // Setup intersection observer for other routes
    const observer = new IntersectionObserver(
      (entries) => {
        entries.forEach(entry => {
          if (entry.isIntersecting) {
            const route = this.routes.find(
              r => r.path === entry.target.getAttribute('data-route')
            )
            if (route) {
              route.component() // Trigger preload
            }
          }
        })
      },
      { rootMargin: '100px' }
    )
    
    // Observe route links
    document.querySelectorAll('[data-route]').forEach(
      el => observer.observe(el)
    )
  }
}

Memory Management

class MemoryManager {
  private memoryThreshold = 0.9 // 90% of available memory
  private gcThreshold = 0.8 // 80% of available memory
  private observer: PerformanceObserver
  
  constructor() {
    this.setupMemoryMonitoring()
  }
  
  private setupMemoryMonitoring() {
    this.observer = new PerformanceObserver((list) => {
      const memory = list.getEntries()[0] as any
      
      if (memory.usedJSHeapSize / memory.jsHeapSizeLimit > this.memoryThreshold) {
        this.handleHighMemoryUsage()
      }
    })
    
    this.observer.observe({ entryTypes: ['memory'] })
  }
  
  private handleHighMemoryUsage() {
    // Clear caches
    this.clearImageCache()
    this.clearResponseCache()
    
    // Force garbage collection if available
    if (window.gc) {
      window.gc()
    }
    
    // Notify monitoring system
    this.notifyMemoryPressure()
  }
  
  private clearImageCache() {
    // Implementation
  }
  
  private clearResponseCache() {
    caches.keys().then(names => {
      names.forEach(name => {
        caches.delete(name)
      })
    })
  }
}

3. Network Optimization

Caching Strategies

interface CacheConfig {
  strategy: 'network-first' | 'cache-first' | 'stale-while-revalidate'
  maxAge: number
  version: string
}

class CacheManager {
  private config: Record<string, CacheConfig> = {
    static: {
      strategy: 'cache-first',
      maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days
      version: 'v1'
    },
    api: {
      strategy: 'stale-while-revalidate',
      maxAge: 60 * 1000, // 1 minute
      version: 'v1'
    }
  }
  
  async fetch(request: Request): Promise<Response> {
    const cacheKey = this.getCacheKey(request)
    const config = this.getConfigForRequest(request)
    
    switch (config.strategy) {
      case 'network-first':
        return this.networkFirst(request, cacheKey, config)
      case 'cache-first':
        return this.cacheFirst(request, cacheKey, config)
      case 'stale-while-revalidate':
        return this.staleWhileRevalidate(request, cacheKey, config)
      default:
        return fetch(request)
    }
  }
  
  private async networkFirst(
    request: Request,
    cacheKey: string,
    config: CacheConfig
  ): Promise<Response> {
    try {
      const response = await fetch(request)
      const cache = await caches.open(config.version)
      cache.put(cacheKey, response.clone())
      return response
    } catch (error) {
      const cachedResponse = await caches.match(cacheKey)
      if (cachedResponse) return cachedResponse
      throw error
    }
  }
  
  private async cacheFirst(
    request: Request,
    cacheKey: string,
    config: CacheConfig
  ): Promise<Response> {
    const cachedResponse = await caches.match(cacheKey)
    if (cachedResponse) {
      // Validate age
      const age = this.getResponseAge(cachedResponse)
      if (age < config.maxAge) {
        return cachedResponse
      }
    }
    
    const response = await fetch(request)
    const cache = await caches.open(config.version)
    cache.put(cacheKey, response.clone())
    return response
  }
  
  private async staleWhileRevalidate(
    request: Request,
    cacheKey: string,
    config: CacheConfig
  ): Promise<Response> {
    const cachedResponse = await caches.match(cacheKey)
    
    const networkPromise = fetch(request).then(response => {
      const cache = await caches.open(config.version)
      cache.put(cacheKey, response.clone())
      return response
    })
    
    return cachedResponse || networkPromise
  }
}

Implementation Guide

Setting Up Performance Monitoring

// Using web-vitals library
import {onCLS, onFID, onLCP} from 'web-vitals';

function sendToAnalytics({name, value, rating}) {
  const body = JSON.stringify({name, value, rating});
  
  // Use Navigator.sendBeacon() for better reliability
  if (navigator.sendBeacon) {
    navigator.sendBeacon('/analytics', body);
  } else {
    fetch('/analytics', {
      method: 'POST',
      body,
      keepalive: true
    });
  }
}

// Monitor Core Web Vitals
onCLS(sendToAnalytics);
onFID(sendToAnalytics);
onLCP(sendToAnalytics);

Real User Monitoring (RUM)

interface PerformanceData {
  navigation: {
    type: string
    timing: PerformanceTiming
  }
  resources: PerformanceResourceTiming[]
  paint: PerformancePaintTiming[]
}

function collectPerformanceData(): PerformanceData {
  return {
    navigation: performance.getEntriesByType('navigation')[0],
    resources: performance.getEntriesByType('resource'),
    paint: performance.getEntriesByType('paint')
  }
}

// Monitor resource loading
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.initiatorType === 'fetch' || entry.initiatorType === 'xmlhttprequest') {
      analyzeFetchPerformance(entry);
    }
  }
});

observer.observe({ entryTypes: ['resource'] });

Implementation Guide: From Theory to Practice

Understanding performance metrics is just the first step. Let's dive into how to implement comprehensive performance monitoring in your applications.

Setting Up Your Monitoring Infrastructure

Before implementing any specific metrics, you need to establish a robust monitoring infrastructure:

  1. Data Collection Layer

    interface PerformanceDataPoint {
      metric: string
      value: number
      timestamp: number
      userAgent: string
      connection: {
        effectiveType: string
        rtt: number
        downlink: number
      }
      device: {
        viewport: {
          width: number
          height: number
        }
        deviceMemory?: number
        hardwareConcurrency?: number
      }
    }
    
    class PerformanceMonitor {
      private buffer: PerformanceDataPoint[] = []
      private flushInterval: number = 5000
      
      constructor() {
        // Flush data periodically
        setInterval(() => this.flush(), this.flushInterval)
        
        // Listen for unload to send remaining data
        window.addEventListener('unload', () => {
          this.flush(true)
        })
      }
      
      addDataPoint(metric: string, value: number) {
        const dataPoint: PerformanceDataPoint = {
          metric,
          value,
          timestamp: Date.now(),
          userAgent: navigator.userAgent,
          connection: this.getConnectionInfo(),
          device: this.getDeviceInfo()
        }
        
        this.buffer.push(dataPoint)
      }
      
      private getConnectionInfo() {
        const conn = (navigator as any).connection
        return {
          effectiveType: conn?.effectiveType || 'unknown',
          rtt: conn?.rtt || 0,
          downlink: conn?.downlink || 0
        }
      }
      
      private getDeviceInfo() {
        return {
          viewport: {
            width: window.innerWidth,
            height: window.innerHeight
          },
          deviceMemory: (navigator as any).deviceMemory,
          hardwareConcurrency: navigator.hardwareConcurrency
        }
      }
      
      private flush(isUnload = false) {
        if (this.buffer.length === 0) return
        
        const data = [...this.buffer]
        this.buffer = []
        
        if (isUnload && navigator.sendBeacon) {
          navigator.sendBeacon('/analytics', JSON.stringify(data))
        } else {
          fetch('/analytics', {
            method: 'POST',
            body: JSON.stringify(data),
            keepalive: true
          })
        }
      }
    }
    
  2. Real User Monitoring Implementation

Real User Monitoring (RUM) provides insights into actual user experiences. Here's how to implement comprehensive RUM:

class RUMCollector {
  private performanceMonitor: PerformanceMonitor
  
  constructor() {
    this.performanceMonitor = new PerformanceMonitor()
    this.setupObservers()
    this.trackUserInteractions()
    this.monitorNetworkRequests()
  }
  
  private setupObservers() {
    // Performance Observer for various metrics
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        switch(entry.entryType) {
          case 'largest-contentful-paint':
            this.handleLCP(entry)
            break
          case 'layout-shift':
            this.handleCLS(entry)
            break
          case 'first-input':
            this.handleFID(entry)
            break
          case 'longtask':
            this.handleLongTask(entry)
            break
          case 'resource':
            this.handleResourceTiming(entry)
            break
        }
      }
    })
    
    // Observe all relevant entry types
    observer.observe({
      entryTypes: [
        'largest-contentful-paint',
        'layout-shift',
        'first-input',
        'longtask',
        'resource'
      ]
    })
  }
  
  private trackUserInteractions() {
    // Track clicks, scrolls, and other interactions
    const interactionEvents = ['click', 'scroll', 'keypress']
    
    interactionEvents.forEach(eventType => {
      window.addEventListener(eventType, (e) => {
        const interaction = {
          type: eventType,
          timestamp: performance.now(),
          target: e.target instanceof Element ? 
            this.getElementPath(e.target) : 'unknown'
        }
        
        this.performanceMonitor.addDataPoint(
          'user-interaction',
          interaction
        )
      }, { passive: true })
    })
  }
  
  private getElementPath(element: Element): string {
    const path = []
    let current = element
    
    while (current && current !== document.body) {
      let identifier = current.id ? 
        `#${current.id}` : 
        current.className ?
          `.${current.className.split(' ').join('.')}` :
          current.tagName.toLowerCase()
          
      path.unshift(identifier)
      current = current.parentElement as Element
    }
    
    return path.join(' > ')
  }
  
  private monitorNetworkRequests() {
    // Monitor fetch requests
    const originalFetch = window.fetch
    window.fetch = async (...args) => {
      const startTime = performance.now()
      try {
        const response = await originalFetch.apply(window, args)
        this.recordNetworkTiming(
          args[0].toString(),
          startTime,
          performance.now(),
          response.status
        )
        return response
      } catch (error) {
        this.recordNetworkTiming(
          args[0].toString(),
          startTime,
          performance.now(),
          0,
          error
        )
        throw error
      }
    }
    
    // Monitor XMLHttpRequest
    const originalXHR = window.XMLHttpRequest
    window.XMLHttpRequest = function() {
      const xhr = new originalXHR()
      const startTime = performance.now()
      
      xhr.addEventListener('load', () => {
        this.recordNetworkTiming(
          xhr.responseURL,
          startTime,
          performance.now(),
          xhr.status
        )
      })
      
      xhr.addEventListener('error', () => {
        this.recordNetworkTiming(
          xhr.responseURL,
          startTime,
          performance.now(),
          0,
          'XHR Error'
        )
      })
      
      return xhr
    }
  }
  
  private recordNetworkTiming(
    url: string,
    startTime: number,
    endTime: number,
    status: number,
    error?: any
  ) {
    const timing = {
      url,
      duration: endTime - startTime,
      status,
      error: error ? error.toString() : undefined
    }
    
    this.performanceMonitor.addDataPoint('network-request', timing)
  }
}

Advanced Monitoring Strategies

  1. Error Tracking and Correlation
interface ErrorContext {
  error: Error
  componentStack?: string
  performanceEntries: PerformanceEntry[]
  userActions: UserAction[]
  networkRequests: NetworkRequest[]
}

class ErrorTracker {
  private static readonly PERFORMANCE_BUFFER_SIZE = 50
  private performanceBuffer: PerformanceEntry[] = []
  private userActionsBuffer: UserAction[] = []
  private networkBuffer: NetworkRequest[] = []
  
  constructor() {
    this.setupPerformanceBuffering()
    this.setupErrorHandling()
  }
  
  private setupPerformanceBuffering() {
    const observer = new PerformanceObserver((list) => {
      const entries = list.getEntries()
      this.performanceBuffer = [
        ...this.performanceBuffer,
        ...entries
      ].slice(-this.PERFORMANCE_BUFFER_SIZE)
    })
    
    observer.observe({ entryTypes: ['resource', 'navigation', 'longtask'] })
  }
  
  private setupErrorHandling() {
    window.onerror = (msg, url, line, col, error) => {
      this.handleError(error || new Error(msg as string))
    }
    
    window.addEventListener('unhandledrejection', (event) => {
      this.handleError(event.reason)
    })
  }
  
  private handleError(error: Error) {
    const errorContext: ErrorContext = {
      error,
      performanceEntries: [...this.performanceBuffer],
      userActions: [...this.userActionsBuffer],
      networkRequests: [...this.networkBuffer]
    }
    
    // Analyze error context
    this.analyzeErrorContext(errorContext)
    
    // Send to error tracking service
    this.reportError(errorContext)
  }
  
  private analyzeErrorContext(context: ErrorContext) {
    // Look for performance issues around error
    const timeWindow = 5000 // 5 seconds
    const errorTime = Date.now()
    
    const relevantPerformance = context.performanceEntries
      .filter(entry => Math.abs(entry.startTime - errorTime) < timeWindow)
    
    const relevantUserActions = context.userActions
      .filter(action => Math.abs(action.timestamp - errorTime) < timeWindow)
    
    // Identify potential causes
    const possibleCauses = this.identifyPossibleCauses(
      relevantPerformance,
      relevantUserActions
    )
    
    context.analysis = {
      possibleCauses,
      performanceCorrelation: this.calculatePerformanceCorrelation(
        relevantPerformance
      )
    }
  }
}

Performance Testing and Validation

Performance testing is crucial for maintaining high standards and preventing regressions. Let's explore different approaches to performance testing and validation.

Unit Testing Performance Metrics

Unit tests for performance metrics help ensure that individual components meet performance budgets. Here's how to implement performance testing with Jest:

import { measureLCP, measureFID, measureCLS } from './performanceMetrics';

describe('Performance Metrics', () => {
  test('LCP should be under 2.5s', async () => {
    const lcp = await measureLCP();
    expect(lcp).toBeLessThan(2500); // 2.5s in milliseconds
  });

  test('FID should be under 100ms', async () => {
    const fid = await measureFID();
    expect(fid).toBeLessThan(100);
  });

  test('CLS should be under 0.1', async () => {
    const cls = await measureCLS();
    expect(cls).toBeLessThan(0.1);
  });
});

Integration Testing with Performance Budgets

Performance budgets help maintain performance standards across your application. Here's how to implement them using Lighthouse CI:

// lighthouserc.js
module.exports = {
  ci: {
    collect: {
      numberOfRuns: 3,
      url: ['http://localhost:3000/'],
    },
    assert: {
      assertions: {
        'first-contentful-paint': ['warn', { maxNumericValue: 2000 }],
        'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
        'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
        'total-blocking-time': ['warn', { maxNumericValue: 300 }],
      },
    },
    upload: {
      target: 'temporary-public-storage',
    },
  },
};

End-to-End Performance Testing

E2E testing helps validate performance in real-world scenarios. Here's an example using Playwright:

import { test, expect } from '@playwright/test';
import { PerformanceObserver } from 'perf_hooks';

test('page load performance', async ({ page }) => {
  // Start performance monitoring
  const performanceMetrics = [];
  const observer = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
      performanceMetrics.push(entry);
    }
  });
  
  observer.observe({ entryTypes: ['navigation', 'resource', 'paint'] });

  // Navigate to page
  await page.goto('http://localhost:3000');

  // Validate performance metrics
  const navigationTiming = await page.evaluate(() => {
    return JSON.stringify(performance.getEntriesByType('navigation')[0]);
  });

  const timing = JSON.parse(navigationTiming);
  expect(timing.domContentLoadedEventEnd - timing.navigationStart).toBeLessThan(2000);
});

Synthetic Monitoring vs Real User Monitoring (RUM)

Both synthetic monitoring and RUM have their place in a comprehensive performance testing strategy:

Synthetic Monitoring

  • Controlled environment testing
  • Consistent baseline measurements
  • Early detection of performance regressions
  • Ideal for CI/CD pipelines
// Example synthetic monitoring setup
class SyntheticMonitor {
  async measurePagePerformance(url: string) {
    const browser = await puppeteer.launch();
    const page = await browser.newPage();
    
    const metrics = await page.metrics();
    const performanceTimings = await page.evaluate(() => ({
      FCP: performance.getEntriesByName('first-contentful-paint')[0]?.startTime,
      LCP: performance.getEntriesByName('largest-contentful-paint')[0]?.startTime,
      TTI: performance.getEntriesByName('time-to-interactive')[0]?.startTime
    }));
    
    await browser.close();
    return { metrics, performanceTimings };
  }
}

Real User Monitoring

  • Actual user experience data
  • Diverse device and network conditions
  • Geographic performance variations
  • Long-term performance trends
// Example RUM implementation
class RUMPerformanceCollector {
  private metrics: PerformanceMetric[] = [];

  constructor() {
    this.initializeObservers();
  }

  private initializeObservers() {
    // Core Web Vitals
    new PerformanceObserver((list) => {
      const entries = list.getEntries();
      entries.forEach(entry => this.processMetric(entry));
    }).observe({ entryTypes: ['largest-contentful-paint', 'first-input', 'layout-shift'] });

    // Navigation and Resource Timing
    new PerformanceObserver((list) => {
      const entries = list.getEntries();
      entries.forEach(entry => this.processMetric(entry));
    }).observe({ entryTypes: ['navigation', 'resource'] });
  }

  private processMetric(entry: PerformanceEntry) {
    this.metrics.push({
      name: entry.name,
      type: entry.entryType,
      startTime: entry.startTime,
      duration: entry.duration,
      timestamp: Date.now()
    });
  }
}
💡

Combine both synthetic monitoring and RUM for the most comprehensive performance testing strategy. Synthetic monitoring provides consistent baselines, while RUM gives you real-world insights.

Optimization Strategies

1. Image Optimization

// Modern image loading
function loadOptimizedImage(src, width, height) {
  const img = document.createElement('img');
  
  // Use srcset for responsive images
  img.srcset = `
    ${src}?w=400 400w,
    ${src}?w=800 800w,
    ${src}?w=1200 1200w
  `;
  
  // Use sizes to help browser choose right image
  img.sizes = '(max-width: 400px) 400px, (max-width: 800px) 800px, 1200px';
  
  // Always set width/height to prevent layout shift
  img.width = width;
  img.height = height;
  
  // Use loading="lazy" for images below the fold
  img.loading = 'lazy';
  
  return img;
}

// Next.js example with automatic optimization
import Image from 'next/image'

function OptimizedImage() {
  return (
    <Image
      src="/hero.jpg"
      width={1200}
      height={600}
      placeholder="blur"
      blurDataURL="data:image/jpeg;base64,..."
      priority={true}  // For above-the-fold images
    />
  )
}

2. JavaScript Performance

// Code splitting example
const HeavyComponent = dynamic(() => import('./HeavyComponent'), {
  loading: () => <LoadingSpinner />,
  ssr: false  // Skip server-side rendering if not needed
})

// Optimize third-party scripts
function loadThirdParty() {
  const script = document.createElement('script')
  script.src = 'https://third-party.com/widget.js'
  script.async = true
  script.defer = true
  
  // Monitor performance impact
  const observer = new PerformanceObserver((list) => {
    const entries = list.getEntries()
    const scriptEntry = entries[entries.length - 1]
    
    if (scriptEntry.duration > 100) {
      console.warn('Third-party script taking too long to execute')
    }
  })
  
  observer.observe({ entryTypes: ['longtask'] })
  document.body.appendChild(script)
}

3. CSS Performance

/* Critical CSS inline in head */
.hero {
  content-visibility: auto;
  contain-intrinsic-size: 0 500px;
}

/* Use @media for conditional loading */
@media (min-width: 768px) {
  @import url('desktop.css');
}

/* Optimize animations */
.animate {
  transform: translateZ(0);  /* Hardware acceleration */
  will-change: transform;    /* Hint to browser */
  transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}

Monitoring Tools Integration

1. Custom Performance Dashboard

interface PerformanceMetrics {
  lcp: number
  fid: number
  cls: number
  ttfb: number
  fcp: number
}

class PerformanceDashboard {
  private metrics: PerformanceMetrics[] = []
  
  addMetric(metric: keyof PerformanceMetrics, value: number) {
    const timestamp = Date.now()
    this.metrics.push({
      ...this.getEmptyMetrics(),
      [metric]: value,
      timestamp
    })
    
    this.analyze()
  }
  
  analyze() {
    const recentMetrics = this.metrics.slice(-100)
    const averages = this.calculateAverages(recentMetrics)
    
    if (averages.lcp > 2500 || averages.cls > 0.1) {
      this.alertPerformanceIssue(averages)
    }
  }
  
  private calculateAverages(metrics: PerformanceMetrics[]): PerformanceMetrics {
    // Implementation
  }
}

2. Automated Performance Testing

# .github/workflows/performance.yml
name: Performance Testing
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lighthouse:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Run Lighthouse
        uses: treosh/lighthouse-ci-action@v9
        with:
          urls: |
            https://your-site.com/
            https://your-site.com/products
          budgetPath: ./budget.json
          uploadArtifacts: true
          
      - name: Check performance budget
        run: |
          if [ ${{ steps.lighthouse.outputs.scores.performance }} -lt 90 ]; then
            echo "Performance score below threshold"
            exit 1
          fi

Case Studies

E-commerce Site Optimization

A major e-commerce site improved their Core Web Vitals:

  • LCP: 3.2s → 1.8s
  • CLS: 0.15 → 0.05
  • FID: 150ms → 80ms

Result: 23% increase in conversion rate

Key strategies:

  1. Image optimization and CDN implementation
  2. Critical CSS extraction
  3. Route-based code splitting
  4. Server-side rendering for product pages

News Website Performance

// Implementing infinite scroll with performance in mind
function InfiniteNewsFeed() {
  const [articles, setArticles] = useState([])
  const [page, setPage] = useState(1)
  
  // Use Intersection Observer for efficient loading
  const observer = useRef(
    new IntersectionObserver(
      (entries) => {
        if (entries[0].isIntersecting) {
          loadMoreArticles()
        }
      },
      { threshold: 0.5 }
    )
  )
  
  async function loadMoreArticles() {
    const newArticles = await fetchArticles(page)
    
    // Optimize DOM updates
    requestAnimationFrame(() => {
      setArticles(prev => [...prev, ...newArticles])
      setPage(prev => prev + 1)
    })
  }
  
  return (
    <div>
      {articles.map(article => (
        <Article
          key={article.id}
          data={article}
          loading="lazy"
        />
      ))}
      <div ref={loadingRef} />
    </div>
  )
}

Advanced Topics

1. Memory Leaks Detection

// Monitor memory usage
const observer = new PerformanceObserver((list) => {
  const entries = list.getEntries();
  const jsHeapSize = entries[0].jsHeapSizeLimit;
  const usedJSHeap = entries[0].usedJSHeapSize;
  
  if (usedJSHeap / jsHeapSize > 0.9) {
    console.warn('High memory usage detected');
    // Implement cleanup strategies
  }
});

observer.observe({ entryTypes: ['memory'] });

// Detect DOM leaks
function detectDOMLeaks() {
  const initialNodes = document.getElementsByTagName('*').length;
  
  return setInterval(() => {
    const currentNodes = document.getElementsByTagName('*').length;
    const diff = currentNodes - initialNodes;
    
    if (diff > 1000) {
      console.warn(`Possible DOM leak: ${diff} new nodes`);
    }
  }, 10000);
}

2. Worker Performance

// Offload heavy computations to Web Worker
const worker = new Worker('worker.js');

interface WorkerTask {
  id: string;
  data: any;
  type: 'HEAVY_COMPUTATION' | 'DATA_PROCESSING';
}

// Main thread
function sendTaskToWorker(task: WorkerTask) {
  const startTime = performance.now();
  
  worker.postMessage(task);
  
  worker.onmessage = (e) => {
    const duration = performance.now() - startTime;
    
    if (duration > 100) {
      console.warn(`Task ${task.id} took ${duration}ms`);
    }
    
    processWorkerResult(e.data);
  };
}

// worker.js
self.onmessage = async (e) => {
  const task = e.data;
  
  // Use transferable objects for better performance
  const result = await processTask(task);
  
  self.postMessage(result, [result.buffer]);
};

Tools and Resources

Ready to optimize your website's performance? Try ScreenScout's Performance Monitoring for free and get detailed insights into your site's performance metrics!