The most surprising thing about Core Web Vitals is that they’re not about your website’s performance, but about your users’ experience of it.

Let’s see this in action. Imagine a user clicking a button to open a modal.

<button id="open-modal">Open Details</button>
<div id="modal" class="hidden">
  <h2>Details</h2>
  <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
  <button id="close-modal">Close</button>
</div>
<script>
  const openButton = document.getElementById('open-modal');
  const modal = document.getElementById('modal');
  const closeButton = document.getElementById('close-modal');

  openButton.addEventListener('click', () => {
    modal.classList.remove('hidden');
    // Simulate a network request for additional data
    setTimeout(() => {
      const detailsContent = modal.querySelector('p');
      detailsContent.textContent = "Here are your details: fetched data.";
      modal.style.height = 'auto'; // Adjust height after content is loaded
    }, 500);
  });

  closeButton.addEventListener('click', () => {
    modal.classList.add('hidden');
  });
</script>
<style>
  .hidden {
    display: none;
  }
  #modal {
    border: 1px solid black;
    padding: 20px;
    margin-top: 20px;
    background-color: white;
    /* Initial fixed height to demonstrate CLS */
    height: 50px;
    overflow: hidden;
  }
</style>

When the user clicks "Open Details," the hidden class is removed, making the modal visible. But then, a setTimeout simulates fetching more data, which updates the paragraph’s content and adjusts the modal’s height. If the initial height: 50px is too small for the final content, the modal’s size will change after it’s already been rendered, pushing other content down the page. This is a classic Cumulative Layout Shift (CLS).

Largest Contentful Paint (LCP) measures when the largest content element (like an image or text block) becomes visible in the viewport. In our example, if the modal’s content was the largest element, its LCP would be recorded when the text "Details" and the paragraph appear. A slow LCP means users wait longer to see the main content.

Interaction to Next Paint (INP) measures the latency of all user interactions with the page. It’s the time from when a user initiates an interaction (like a click or keypress) to when the browser visually responds. Our modal opening, the simulated data fetch, and the subsequent DOM update all contribute to INP. A high INP indicates that the page feels sluggish and unresponsive.

Optimizing these metrics is about making the user’s experience feel fast and stable.

For LCP, the goal is to get that largest content element on screen quickly. This means:

  • Server-Side Rendering (SSR) or Static Site Generation (SSG): Serving pre-rendered HTML means the browser doesn’t have to wait for JavaScript to build the page.
  • Optimize Images: Use modern formats like WebP, compress images, and use srcset to serve appropriately sized images for different screen resolutions.
  • Lazy Loading: For content below the fold, defer loading until it’s about to enter the viewport.
  • Prioritize Critical Resources: Use <link rel="preload"> for critical fonts or stylesheets needed for the LCP element.
  • Reduce Server Response Time: Faster TTFB (Time To First Byte) means the browser gets the HTML sooner.

For INP, the focus is on smooth interactions. This involves:

  • Break Up Long-Running Tasks: JavaScript that blocks the main thread for too long will delay responses to user input. Use setTimeout(..., 0) or requestIdleCallback to break up large operations.
  • Optimize Event Handlers: Ensure your event listeners are efficient and don’t perform heavy computations synchronously.
  • Code Splitting: Load only the JavaScript needed for the current interaction.
  • Reduce Third-Party Script Impact: Analyze and defer or optimize scripts from analytics, ads, or widgets that can slow down interactions.
  • Web Workers: Offload computationally intensive tasks to background threads so they don’t block the main thread.

For CLS, the objective is to prevent unexpected shifts in layout.

  • Specify Dimensions: Always provide width and height attributes for images and video elements, or reserve space using CSS aspect-ratio.
  • Avoid Inserting Content Above Existing Content: As seen in our modal example, adding new elements dynamically where they push existing content down is a primary cause of CLS.
  • Preload Fonts: Ensure fonts are loaded before text is rendered to avoid "flash of unstyled text" (FOUT) or "flash of invisible text" (FOIT) that can cause reflows. Use font-display: optional or swap.
  • Reserve Space for Ads and Embeds: If ads or iframes are loaded dynamically, reserve their potential space beforehand.

The most common pitfall for CLS is dynamic content loading that doesn’t reserve space, leading to elements jumping around as they appear. In our modal example, the initial height: 50px was a placeholder, but the content update caused a reflow because the final content was taller than the placeholder. By setting a more appropriate min-height or handling the height change more gracefully, you can mitigate this.

The next challenge you’ll encounter is understanding how these metrics are measured and reported by tools like PageSpeed Insights and Chrome User Experience Report (CrUX).

Want structured learning?

Take the full Performance course →