Core Web Vitals

A comprehensive guide to understanding, measuring, and optimizing LCP, FID, and CLS for better user experience.

โšก

LCP - Largest Contentful Paint

Measures loading performance

What is LCP?

LCP measures how long it takes for the largest visible content element to render on the screen. It answers the user's question: "When did the main content of this page load?"

LCP Time Rating
โ‰ค 2.5 seconds ๐ŸŸข Good
2.5 - 4.0 seconds ๐ŸŸก Needs Improvement
> 4.0 seconds ๐Ÿ”ด Poor

What Elements Can Be the LCP?

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Logo (small) โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ Navigation โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ HERO IMAGE โ”‚ โ—„โ”€โ”€โ”€ LCP Element โ”‚ โ”‚ (Largest visible) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ Some text below... โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

What Causes Poor LCP?

1. Slow Server Response Time

The browser can't render anything until it receives HTML from your server.

User Request โ”€โ”€โ”€โ”€โ”€โ”€โ–บ Server Processing โ”€โ”€โ”€โ”€โ”€โ”€โ–บ HTML Response โ”€โ”€โ”€โ”€โ”€โ”€โ–บ Render โ–ฒ โ”‚ If this is slow, everything is delayed

2. Render-Blocking Resources

The browser stops rendering when it encounters certain resources:

<head>
  <!-- These BLOCK rendering until fully downloaded & parsed -->
  <link rel="stylesheet" href="styles.css">
  <script src="app.js"></script>
</head>

3. Slow Resource Load Times

4. Client-Side Rendering

Traditional (Server-Rendered): HTML arrives โ”€โ”€โ–บ Content visible โ”€โ”€โ–บ LCP โœ“ Client-Side Rendered (CSR): HTML arrives (empty) โ”€โ”€โ–บ Download JS โ”€โ”€โ–บ Parse JS โ”€โ”€โ–บ Execute โ”€โ”€โ–บ Fetch Data โ”€โ”€โ–บ Render โ”€โ”€โ–บ LCP โœ— โ”‚ Way too late!

How to Measure LCP

Lab Tools

Field Tools

Measuring in Code

// Basic LCP measurement using Performance Observer API
const observer = new PerformanceObserver((list) => {
  const entries = list.getEntries();
  
  // LCP can change as page loads, so we want the last one
  const lastEntry = entries[entries.length - 1];
  
  console.log('LCP:', lastEntry.startTime, 'ms');
  console.log('LCP Element:', lastEntry.element);
  console.log('LCP Size:', lastEntry.size);
});

observer.observe({ 
  type: 'largest-contentful-paint', 
  buffered: true  // get entries from before observer was created
});
๐Ÿ’ก Quick Debug Tip

Paste this in console to highlight the LCP element:

new PerformanceObserver((list) => {
  const lcp = list.getEntries().pop();
  lcp.element.style.outline = '5px solid red';
}).observe({ type: 'largest-contentful-paint', buffered: true });

How to Optimize LCP

1. Preload the LCP Image

<head>
  <!-- Tell browser: "This image is critical, start loading NOW!" -->
  <link 
    rel="preload" 
    as="image" 
    href="/hero-image.webp"
  >
</head>

2. Use fetchpriority Attribute

<!-- High priority for LCP image -->
<img 
  src="hero.webp" 
  alt="Hero" 
  fetchpriority="high"
>

<!-- Low priority for below-fold images -->
<img 
  src="footer.webp" 
  alt="Footer" 
  fetchpriority="low" 
  loading="lazy"
>

3. Inline Critical CSS

<head>
  <!-- Critical CSS inline - renders immediately, no network request -->
  <style>
    /* Only above-the-fold styles */
    .header { ... }
    .hero { ... }
  </style>
  
  <!-- Load full CSS without blocking rendering -->
  <link 
    rel="preload" 
    href="styles.css" 
    as="style" 
    onload="this.rel='stylesheet'"
  >
</head>

4. Defer Non-Critical JavaScript

<!-- defer: Doesn't block, executes after HTML parsing -->
<script src="app.js" defer></script>

<!-- async: Doesn't block, executes as soon as downloaded -->
<script src="analytics.js" async></script>

5. Optimize Web Fonts

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

<style>
  @font-face {
    font-family: 'MyFont';
    src: url('/fonts/main.woff2') format('woff2');
    
    /* swap: Show fallback immediately, swap when font loads */
    font-display: swap;
  }
</style>
LCP Optimization Checklist
  • Add <link rel="preload"> for LCP image
  • Add fetchpriority="high" to LCP image
  • Convert images to WebP/AVIF
  • Inline critical CSS
  • Add defer to non-critical scripts
  • Use font-display: swap for fonts
  • Set up a CDN
  • Enable compression (gzip/brotli)
๐Ÿ‘†

FID - First Input Delay

Measures interactivity

What is FID?

FID measures the delay between when a user first interacts with your page and when the browser can respond. It answers: "Why isn't this button working when I click it?"

๐Ÿ“ Note

FID has been replaced by INP (Interaction to Next Paint) as of March 2024. However, the concepts still apply.

FID Time Rating
โ‰ค 100 ms ๐ŸŸข Good
100 - 300 ms ๐ŸŸก Needs Improvement
> 300 ms ๐Ÿ”ด Poor

The Core Problem

FID is almost always caused by JavaScript blocking the main thread. When JavaScript is executing, the browser cannot respond to user input.

Main Thread: โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Parse HTML โ”‚ โ”‚ Execute JavaScript โ”‚ โ”‚ Idle โ”‚ โ”‚ (50ms) โ”‚ โ”‚ (400ms) โ† BLOCKING! โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ–ฒ โ”‚ User clicks here โ”‚ โ”œโ”€โ”€ FID = 250ms โ”€โ”€โ–บโ”‚ (waiting for JS to finish)

What Counts as "First Input"?

โœ… Measured by FID
  • Clicks
  • Taps
  • Key presses
โŒ NOT Measured
  • Scrolling
  • Zooming
  • Pinching
๐Ÿ“

CLS - Cumulative Layout Shift

Measures visual stability

What is CLS?

CLS measures how much page content unexpectedly moves around while loading. It answers the user's frustration: "I was about to click that button and it moved!"

CLS Score Rating
โ‰ค 0.1 ๐ŸŸข Good
0.1 - 0.25 ๐ŸŸก Needs Improvement
> 0.25 ๐Ÿ”ด Poor
๐Ÿ“Š Note

CLS is a unitless score, not milliseconds. It's calculated as: CLS = Impact Fraction ร— Distance Fraction

The Classic CLS Problem

STEP 1: Page loads, user sees button โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Article Title โ”‚ โ”‚ Some text content here... โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Buy Now ๐Ÿ–ฑ๏ธ โ”‚ โ† User moves โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ cursor here โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ STEP 2: Ad loads, pushes content down โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Article Title โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ ADVERTISEMENT โ”‚ โ† Loads late! โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ Some text content here... โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Buy Now โ”‚ โ† Button moved โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ STEP 3: User clicks... but button moved! ๐Ÿ–ฑ๏ธ โ† Click lands on ad instead! ๐Ÿ˜ก

What Causes Poor CLS?

1. Images Without Dimensions

โŒ Bad
<!-- No dimensions = 0 initial height -->
<img src="photo.jpg">
โœ… Good
<img 
  src="photo.jpg"
  width="800"
  height="600"
>

2. Ads/Embeds Without Reserved Space

Ads load late and dynamically inject content, shifting everything below.

3. Web Fonts Causing Text Reflow

Different fonts have different sizes, causing text to reflow when custom font loads.

4. Dynamically Injected Content

5. Animations That Trigger Layout

โŒ Bad - Triggers Layout
element.style.height = '200px';
element.style.top = '50px';
element.style.margin = '10px';
โœ… Good - Compositing Only
element.style.transform = 
  'translateY(50px)';
element.style.opacity = '0.5';

How to Measure CLS

Debug: Highlight Shifting Elements

// Paste in console to highlight elements that shift
new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    // Skip user-initiated shifts
    if (entry.hadRecentInput) continue;
    
    entry.sources?.forEach((source) => {
      if (source.node) {
        source.node.style.outline = '3px solid red';
        console.log('Shifted:', source.node);
      }
    });
  }
}).observe({ type: 'layout-shift', buffered: true });

How to Optimize CLS

1. Always Set Image Dimensions

<!-- Option 1: Explicit width and height -->
<img 
  src="photo.jpg" 
  width="800"
  height="600"
>

<!-- Option 2: CSS aspect-ratio -->
<style>
.image {
  width: 100%;
  aspect-ratio: 16 / 9;  /* Reserve space */
  object-fit: cover;
}
</style>

2. Reserve Space for Ads

.ad-slot {
  /* Reserve minimum space based on ad size */
  min-height: 250px;
  width: 300px;
  background-color: #f5f5f5;
}

3. Prevent Font Layout Shifts

@font-face {
  font-family: 'BrandFont';
  src: url('/fonts/brand.woff2') format('woff2');
  
  /* 'optional' = best for CLS, only uses font if cached */
  font-display: optional;
}

4. Use Fixed Positioning for Banners

.cookie-banner {
  position: fixed;  /* Removed from document flow */
  bottom: 0;
  left: 0;
  right: 0;
  
  /* Animate with transform - no CLS */
  transform: translateY(0);
  transition: transform 0.3s;
}

.cookie-banner.hidden {
  transform: translateY(100%);
}

5. Handle Unknown Image Dimensions

// Detect dimensions before adding to DOM
function loadImage(src, container) {
  const img = new Image();
  
  img.onload = function() {
    // Set aspect ratio from natural dimensions
    const ratio = this.naturalWidth / this.naturalHeight;
    container.style.aspectRatio = ratio;
    
    // Now add image - no layout shift!
    container.appendChild(this);
  };
  
  img.src = src;
}
CLS Optimization Checklist
  • Add width/height to ALL images
  • Add aspect-ratio to image containers
  • Reserve space for ad slots with min-height
  • Wrap iframes in aspect-ratio containers
  • Use font-display: optional for fonts
  • Use fixed/absolute positioning for banners
  • Implement skeleton screens
  • Use transform instead of layout properties for animations