Skip to main content
โšก Calmops

Web Vitals and Performance Metrics in JavaScript

Web Vitals and Performance Metrics in JavaScript

Web Vitals are Google’s metrics for measuring user experience. This article covers understanding, measuring, and optimizing these critical metrics.

Introduction

Web Vitals measure:

  • Loading performance (LCP)
  • Interactivity (FID, INP)
  • Visual stability (CLS)

Understanding Web Vitals helps you:

  • Improve user experience
  • Rank better in search results
  • Reduce bounce rates
  • Increase conversions

Core Web Vitals

Largest Contentful Paint (LCP)

// LCP: Time when largest content element is visible
// Target: < 2.5 seconds

// Measuring LCP
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log('LCP:', entry.renderTime || entry.loadTime);
  }
});

observer.observe({ entryTypes: ['largest-contentful-paint'] });
// Practical example: Monitoring LCP
class LCPMonitor {
  constructor() {
    this.lcpValue = null;
  }

  start() {
    const observer = new PerformanceObserver((list) => {
      const entries = list.getEntries();
      const lastEntry = entries[entries.length - 1];
      this.lcpValue = lastEntry.renderTime || lastEntry.loadTime;
      
      console.log('LCP updated:', this.lcpValue);
      
      // Send to analytics
      this.reportMetric('LCP', this.lcpValue);
    });

    observer.observe({ entryTypes: ['largest-contentful-paint'] });
  }

  reportMetric(name, value) {
    // Send to analytics service
    fetch('/api/metrics', {
      method: 'POST',
      body: JSON.stringify({ name, value })
    });
  }

  getValue() {
    return this.lcpValue;
  }
}

// Usage
const lcpMonitor = new LCPMonitor();
lcpMonitor.start();

First Input Delay (FID)

// FID: Time from user input to browser response
// Target: < 100 milliseconds

// Measuring FID
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log('FID:', entry.processingDuration);
  }
});

observer.observe({ entryTypes: ['first-input'] });
// Practical example: Monitoring FID
class FIDMonitor {
  constructor() {
    this.fidValue = null;
  }

  start() {
    const observer = new PerformanceObserver((list) => {
      const entries = list.getEntries();
      const firstEntry = entries[0];
      this.fidValue = firstEntry.processingDuration;
      
      console.log('FID:', this.fidValue);
      
      // Send to analytics
      this.reportMetric('FID', this.fidValue);
    });

    observer.observe({ entryTypes: ['first-input'] });
  }

  reportMetric(name, value) {
    fetch('/api/metrics', {
      method: 'POST',
      body: JSON.stringify({ name, value })
    });
  }

  getValue() {
    return this.fidValue;
  }
}

// Usage
const fidMonitor = new FIDMonitor();
fidMonitor.start();

Cumulative Layout Shift (CLS)

// CLS: Measure of visual stability
// Target: < 0.1

// Measuring CLS
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (!entry.hadRecentInput) {
      console.log('CLS:', entry.value);
    }
  }
});

observer.observe({ entryTypes: ['layout-shift'] });
// Practical example: Monitoring CLS
class CLSMonitor {
  constructor() {
    this.clsValue = 0;
  }

  start() {
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (!entry.hadRecentInput) {
          this.clsValue += entry.value;
          console.log('CLS updated:', this.clsValue);
          
          // Send to analytics
          this.reportMetric('CLS', this.clsValue);
        }
      }
    });

    observer.observe({ entryTypes: ['layout-shift'] });
  }

  reportMetric(name, value) {
    fetch('/api/metrics', {
      method: 'POST',
      body: JSON.stringify({ name, value })
    });
  }

  getValue() {
    return this.clsValue;
  }
}

// Usage
const clsMonitor = new CLSMonitor();
clsMonitor.start();

Additional Web Vitals

Interaction to Next Paint (INP)

// INP: Measures responsiveness to all interactions
// Target: < 200 milliseconds

// Measuring INP
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log('INP:', entry.duration);
  }
});

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

Time to First Byte (TTFB)

// TTFB: Time from request to first byte received
// Target: < 600 milliseconds

// Measuring TTFB
const perfData = performance.getEntriesByType('navigation')[0];
const ttfb = perfData.responseStart - perfData.fetchStart;

console.log('TTFB:', ttfb);

Comprehensive Metrics Monitoring

Web Vitals Library

// Using web-vitals library
import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals';

function sendMetric(metric) {
  // Send to analytics
  fetch('/api/metrics', {
    method: 'POST',
    body: JSON.stringify(metric)
  });
}

getCLS(sendMetric);
getFID(sendMetric);
getFCP(sendMetric);
getLCP(sendMetric);
getTTFB(sendMetric);

Custom Metrics Dashboard

// Create comprehensive metrics dashboard
class MetricsDashboard {
  constructor() {
    this.metrics = {
      LCP: null,
      FID: null,
      CLS: 0,
      TTFB: null,
      FCP: null,
      DCL: null,
      Load: null
    };
  }

  start() {
    this.measureNavigationTiming();
    this.measureLCP();
    this.measureFID();
    this.measureCLS();
  }

  measureNavigationTiming() {
    window.addEventListener('load', () => {
      const perfData = performance.getEntriesByType('navigation')[0];
      
      this.metrics.TTFB = perfData.responseStart - perfData.fetchStart;
      this.metrics.FCP = performance.getEntriesByName('first-contentful-paint')[0]?.startTime;
      this.metrics.DCL = perfData.domContentLoadedEventEnd - perfData.fetchStart;
      this.metrics.Load = perfData.loadEventEnd - perfData.fetchStart;
      
      this.reportMetrics();
    });
  }

  measureLCP() {
    const observer = new PerformanceObserver((list) => {
      const entries = list.getEntries();
      const lastEntry = entries[entries.length - 1];
      this.metrics.LCP = lastEntry.renderTime || lastEntry.loadTime;
      this.reportMetrics();
    });

    observer.observe({ entryTypes: ['largest-contentful-paint'] });
  }

  measureFID() {
    const observer = new PerformanceObserver((list) => {
      const firstEntry = list.getEntries()[0];
      this.metrics.FID = firstEntry.processingDuration;
      this.reportMetrics();
    });

    observer.observe({ entryTypes: ['first-input'] });
  }

  measureCLS() {
    let clsValue = 0;
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (!entry.hadRecentInput) {
          clsValue += entry.value;
          this.metrics.CLS = clsValue;
          this.reportMetrics();
        }
      }
    });

    observer.observe({ entryTypes: ['layout-shift'] });
  }

  reportMetrics() {
    console.log('Current Metrics:', this.metrics);
    
    // Send to analytics
    fetch('/api/metrics', {
      method: 'POST',
      body: JSON.stringify(this.metrics)
    });
  }

  getMetrics() {
    return this.metrics;
  }
}

// Usage
const dashboard = new MetricsDashboard();
dashboard.start();

Optimizing Web Vitals

Improving LCP

// 1. Optimize server response time
// - Use CDN
// - Implement caching
// - Optimize database queries

// 2. Reduce CSS blocking time
// - Inline critical CSS
// - Defer non-critical CSS

// 3. Reduce JavaScript blocking time
// - Code split
// - Lazy load
// - Defer non-critical scripts

// 4. Optimize images
// - Use modern formats (WebP)
// - Responsive images
// - Lazy load images

// Example: Lazy load images
const images = document.querySelectorAll('img[data-src]');
const imageObserver = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;
      imageObserver.unobserve(img);
    }
  });
});

images.forEach(img => imageObserver.observe(img));

Improving FID

// 1. Break up long tasks
// - Use requestIdleCallback
// - Use setTimeout to yield to browser

// 2. Use web workers
// - Offload heavy computation
// - Keep main thread responsive

// 3. Reduce JavaScript
// - Code split
// - Remove unused code
// - Use smaller libraries

// Example: Break up long task
function heavyComputation() {
  const tasks = [];
  
  // Split work into chunks
  for (let i = 0; i < 1000; i++) {
    tasks.push(() => {
      // Do work
      Math.sqrt(i);
    });
  }
  
  // Process tasks with delays
  let taskIndex = 0;
  
  function processTasks() {
    const startTime = performance.now();
    
    while (taskIndex < tasks.length && performance.now() - startTime < 5) {
      tasks[taskIndex]();
      taskIndex++;
    }
    
    if (taskIndex < tasks.length) {
      setTimeout(processTasks, 0);
    }
  }
  
  processTasks();
}

Improving CLS

// 1. Avoid layout shifts
// - Set size for images and videos
// - Avoid inserting content above existing content
// - Use transform instead of layout properties

// 2. Use font-display: swap
// - Prevent font swap layout shift

// 3. Avoid animations that cause layout shifts
// - Use transform and opacity
// - Avoid animating layout properties

// Example: Prevent image layout shift
// โœ… Good: Set dimensions
<img src="image.jpg" width="400" height="300" />

// โŒ Bad: No dimensions
<img src="image.jpg" />

// Example: Use transform instead of position
// โœ… Good: Use transform
element.style.transform = 'translateX(10px)';

// โŒ Bad: Use left property
element.style.left = '10px';

Performance Budgets

Setting Budgets

// performance-budget.json
{
  "bundles": [
    {
      "name": "main",
      "maxSize": "250kb"
    },
    {
      "name": "vendor",
      "maxSize": "200kb"
    }
  ],
  "metrics": [
    {
      "name": "LCP",
      "maxValue": 2500
    },
    {
      "name": "FID",
      "maxValue": 100
    },
    {
      "name": "CLS",
      "maxValue": 0.1
    }
  ]
}

Enforcing Budgets

// scripts/check-performance-budget.js
const fs = require('fs');
const path = require('path');

function checkPerformanceBudget() {
  const budget = JSON.parse(
    fs.readFileSync('performance-budget.json', 'utf8')
  );
  
  const metrics = {
    LCP: 2300,
    FID: 95,
    CLS: 0.08
  };
  
  let passed = true;
  
  budget.metrics.forEach(metric => {
    if (metrics[metric.name] > metric.maxValue) {
      console.error(`โŒ ${metric.name} exceeds budget: ${metrics[metric.name]} > ${metric.maxValue}`);
      passed = false;
    } else {
      console.log(`โœ… ${metric.name} within budget: ${metrics[metric.name]} < ${metric.maxValue}`);
    }
  });
  
  if (!passed) {
    process.exit(1);
  }
}

checkPerformanceBudget();

Monitoring in Production

Analytics Integration

// Send metrics to analytics service
class AnalyticsReporter {
  constructor(endpoint) {
    this.endpoint = endpoint;
  }

  reportMetric(name, value) {
    // Use sendBeacon for reliability
    const data = JSON.stringify({
      name,
      value,
      url: window.location.href,
      timestamp: Date.now()
    });
    
    navigator.sendBeacon(this.endpoint, data);
  }

  reportWebVitals() {
    import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
      getCLS(metric => this.reportMetric('CLS', metric.value));
      getFID(metric => this.reportMetric('FID', metric.value));
      getFCP(metric => this.reportMetric('FCP', metric.value));
      getLCP(metric => this.reportMetric('LCP', metric.value));
      getTTFB(metric => this.reportMetric('TTFB', metric.value));
    });
  }
}

// Usage
const reporter = new AnalyticsReporter('/api/metrics');
reporter.reportWebVitals();

Best Practices

  1. Monitor all Web Vitals:

    // โœ… Good
    getCLS(sendMetric);
    getFID(sendMetric);
    getLCP(sendMetric);
    
  2. Set performance budgets:

    // โœ… Good
    // LCP < 2.5s, FID < 100ms, CLS < 0.1
    
  3. Optimize for real users:

    // โœ… Good
    // Monitor in production
    // Use real user metrics
    
  4. Test on slow networks:

    // โœ… Good
    // Test on 3G, 4G networks
    // Use DevTools throttling
    

Common Mistakes

  1. Only testing on fast networks:

    // โŒ Bad - only test on fast connection
    // โœ… Good - test on slow networks too
    
  2. Ignoring real user metrics:

    // โŒ Bad - only use lab metrics
    // โœ… Good - monitor real users
    
  3. Not setting budgets:

    // โŒ Bad - no performance targets
    // โœ… Good - set and enforce budgets
    

Summary

Web Vitals are essential for user experience. Key takeaways:

  • Measure LCP (< 2.5s), FID (< 100ms), CLS (< 0.1)
  • Monitor in production with real users
  • Set and enforce performance budgets
  • Optimize for slow networks
  • Use web-vitals library for easy measurement
  • Report metrics to analytics
  • Continuously improve metrics

Next Steps

Comments