Skip to content
All essays
WebMarch 27, 202414 min

Mastering Web Vitals: Core Performance Metrics Guide

Deep dive into Core Web Vitals - LCP, FID, CL, and how to optimize them for better user experience and SEO rankings

Ü
Ümit Uz
Mobile & Full Stack Developer

Core Web Vitals are the subset of Web Vitals that directly impact user experience and are used by Google for search ranking. Understanding and optimizing these metrics is crucial for modern web development.

Understanding Core Web Vitals

Core Web Vitals consist of three specific metrics:

  1. 1Largest Contentful Paint (LCP) - Loading performance
  2. 2First Input Delay (FID) - Interactivity
  3. 3Cumulative Layout Shift (CLS) - Visual stability

Largest Contentful Paint (LCP)

LCP measures the time from when the user initiates loading the page to when the largest image or text block is rendered within the viewport.

Measuring LCP

javascript
// Measure LCP in JavaScript
new PerformanceObserver((entryList) => {
  const entries = entryList.getEntries();
  const lastEntry = entries[entries.length - 1];
  console.log('LCP:', lastEntry.startTime);
}).observe({ entryTypes: ['largest-contentful-paint'] });

Optimizing LCP

1. Optimize Images

html
<!-- ❌ Unoptimized -->
<img src="hero-image.jpg" alt="Hero">

<!-- ✅ Optimized with modern formats -->
<picture>
  <source srcset="hero-image.webp" type="image/webp">
  <source srcset="hero-image.avif" type="image/avif">
  <img
    src="hero-image.jpg"
    loading="eager"
    decoding="sync"
    fetchpriority="high"
    width="1200"
    height="600"
    alt="Hero"
  >
</picture>

2. Preload Critical Resources

html
<head>
  <!-- Preload critical CSS -->
  <link rel="preload" href="/styles/main.css" as="style">

  <!-- Preload critical fonts -->
  <link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>

  <!-- Preload critical JavaScript -->
  <link rel="preload" href="/scripts/main.js" as="script">

  <!-- Preconnect to external origins -->
  <link rel="preconnect" href="https://cdn.example.com">
  <link rel="dns-prefetch" href="https://api.example.com">
</head>

3. Server-Side Rendering

javascript
// Next.js example with SSR optimization
export async function getServerSideProps() {
  // Fetch critical data on the server
  const criticalData = await fetchCriticalData();

  // Return data for immediate rendering
  return {
    props: {
      criticalData,
    },
  };
}

// Use streaming for faster initial render
import { Suspense } from 'react';

export default function Page() {
  return (
    <div>
      <Header />
      <Suspense fallback={<Skeleton />}>
        <LazyComponent />
      </Suspense>
    </div>
  );
}

4. Critical CSS Inlining

html
<style>
  /* Inline critical CSS for above-the-fold content */
  .hero { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
  .hero-title { font-size: 3rem; font-weight: bold; color: white; }
</style>

<!-- Load non-critical CSS asynchronously -->
<link rel="preload" href="/styles/main.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/styles/main.css"></noscript>

First Input Delay (FID)

FID measures the time from when a user first interacts with your site to when the browser can respond to that interaction.

Measuring FID

javascript
// Measure FID
new PerformanceObserver((entryList) => {
  for (const entry of entryList.getEntries()) {
    console.log('FID:', entry.processingStart - entry.startTime);
  }
}).observe({ entryTypes: ['first-input'] });

Optimizing FID

1. Reduce JavaScript Execution Time

javascript
// ❌ Blocking JavaScript
document.addEventListener('DOMContentLoaded', () => {
  // Heavy computation blocks main thread
  for (let i = 0; i < 1000000; i++) {
    processItem(items[i]);
  }
});

// ✅ Non-blocking with code splitting
import('https://cdn.skypack.dev/heavy-computation').then((module) => {
  module.processItems(items);
});

// ✅ Use Web Workers for CPU-intensive tasks
const worker = new Worker('heavy-worker.js');
worker.postMessage({ items });
worker.onmessage = (e) => {
  displayResults(e.data);
};

2. Defer Non-Critical JavaScript

html
<!-- Defer non-critical scripts -->
<script defer src="/scripts/analytics.js"></script>
<script defer src="/scripts/widgets.js"></script>

<!-- Load critical scripts normally -->
<script src="/scripts/critical.js"></script>

<!-- Use type="module" for modern browsers -->
<script type="module" src="/scripts/app.js"></script>

<!-- Fallback for older browsers -->
<script nomodule src="/scripts/app-legacy.js"></script>

3. Optimize Event Handlers

javascript
// ❌ Expensive event handler
input.addEventListener('input', (e) => {
  // Heavy computation on every keystroke
  const results = heavySearch(e.target.value);
  displayResults(results);
});

// ✅ Debounced event handler
const debounce = (fn, delay) => {
  let timeoutId;
  return (...args) => {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => fn(...args), delay);
  };
};

input.addEventListener('input', debounce((e) => {
  const results = heavySearch(e.target.value);
  displayResults(results);
}, 300));

Cumulative Layout Shift (CLS)

CLS measures the sum total of all individual layout shift scores for every unexpected layout shift that occurs during the entire lifespan of the page.

Measuring CLS

javascript
let clsValue = 0;
new PerformanceObserver((entryList) => {
  for (const entry of entryList.getEntries()) {
    if (!entry.hadRecentInput) {
      clsValue += entry.value;
      console.log('CLS:', clsValue);
    }
  }
}).observe({ entryTypes: ['layout-shift'] });

Optimizing CLS

1. Reserve Space for Dynamic Content

css
/* ❌ No space reserved */
.hero-image {
  /* Content will shift when image loads */
}

/* ✅ Reserve space for images */
.hero-image {
  aspect-ratio: 16 / 9;
  width: 100%;
  background-color: #f0f0f0;
}

/* ✅ Reserve space for ads */
.ad-container {
  min-height: 250px;
  min-width: 300px;
}

/* ✅ Reserve space for dynamic content */
.dynamic-content {
  min-height: 200px;
  display: flex;
  align-items: center;
  justify-content: center;
}

2. Prevent Layout Shifts from Fonts

``html <!-- Use font-display: swap --> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap" >

<style> @font-face { font-family: 'CustomFont'; src: url('/fonts/custom.woff2') format('woff2'); font-display: swap; /* Prevents FOIT */ } </style>

/* Reserve space for text */ .heading { font-family: 'CustomFont', sans-serif; font-size: 2rem; min-height: 1.2em; /* Reserve space based on font size */ line-height: 1.2; }


#### 3. Handle Dynamic Content Insertions

// ❌ Content insertion causes shift function loadContent() { const container = document.getElementById('container'); // Inserting content shifts existing elements container.innerHTML = '<div class="new-content">...</div>'; }

// ✅ Pre-reserve space for dynamic content function loadContent() { const container = document.getElementById('container'); // Show loading indicator in reserved space container.innerHTML = '<div class="loading" style="min-height: 200px">Loading...</div>';

// Replace with actual content fetchData().then(data => { container.innerHTML = '<div class="new-content">...</div>'; }); }


## Advanced Performance Monitoring

### Real User Monitoring (RUM)

class PerformanceMonitor { constructor() { this.metrics = {}; this.init(); }

init() { // Measure page load window.addEventListener('load', () => { setTimeout(() => { this.metrics.pageLoad = performance.timing.loadEventEnd - performance.timing.navigationStart; this.metrics.domContentLoaded = performance.timing.domContentLoadedEventEnd - performance.timing.navigationStart; this.sendMetrics(); }, 0); });

// Measure LCP new PerformanceObserver((list) => { const entries = list.getEntries(); const lastEntry = entries[entries.length - 1]; this.metrics.lcp = lastEntry.startTime; }).observe({ entryTypes: ['largest-contentful-paint'] });

// Measure FID new PerformanceObserver((list) => { for (const entry of list.getEntries()) { this.metrics.fid = entry.processingStart - entry.startTime; } }).observe({ entryTypes: ['first-input'] });

// Measure CLS let clsValue = 0; new PerformanceObserver((list) => { for (const entry of list.getEntries()) { if (!entry.hadRecentInput) { clsValue += entry.value; this.metrics.cls = clsValue; } } }).observe({ entryTypes: ['layout-shift'] }); }

sendMetrics() { // Send to analytics service navigator.sendBeacon('/api/analytics', JSON.stringify(this.metrics)); } }

// Initialize monitoring new PerformanceMonitor();


### Performance Budgets

// webpack.config.js const SpeedMeasurePlugin = require('speed-measure-webpack-plugin'); const smp = new SpeedMeasurePlugin();

module.exports = smp.wrap({ performance: { maxEntrypointSize: 244000, // 244KB maxAssetSize: 244000, hints: 'warning' }, plugins: [ new webpack.BundleAnalyzerPlugin({ analyzerMode: 'static', openAnalyzer: false }) ] });


## Tools for Measuring Web Vitals

### Lighthouse CI

// lighthouserc.js module.exports = { ci: { collect: { url: [ 'https://example.com', 'https://example.com/page1', 'https://example.com/page2' ], numberOfRuns: 3 }, assert: { assertions: { 'categories:performance': ['error', { minScore: 0.9 }], 'categories:accessibility': ['error', { minScore: 0.9 }], 'categories:best-practices': ['error', { minScore: 0.9 }], 'categories:seo': ['error', { minScore: 0.9 }], 'first-contentful-paint': ['warn', { maxNumericValue: 2000 }], 'largest-contentful-paint': ['error', { maxNumericValue: 2500 }], 'cumulative-layout-shift': ['warn', { maxNumericValue: 0.1 }], 'total-blocking-time': ['warn', { maxNumericValue: 300 }] } }, upload: { target: 'temporary-public-storage' } } };


### Web Vitals JavaScript Library

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

getCLS(console.log); getFID(console.log); getFCP(console.log); getLCP(console.log); getTTFB(console.log);


## Best Practices

1. **Set Performance Budgets**: Establish and enforce budgets for bundle sizes, metrics
2. **Monitor Real Users**: Use RUM alongside lab testing
3. **Continuous Optimization**: Make performance optimization part of your development workflow
4. **Mobile-First**: Optimize for mobile devices first, then enhance for desktop
5. **Progressive Enhancement**: Start with core functionality, then add features
6. **Regular Audits**: Run performance audits regularly to catch regressions

## Conclusion

Core Web Vitals are essential for delivering excellent user experiences and maintaining good SEO rankings. Focus on optimizing LCP, FID, and CLS through proper resource loading, code splitting, and careful content layout. Regular monitoring and testing ensure your performance improvements stay effective over time.

Related essays

Next essay
Advanced Database Performance Optimization Techniques