EthicsComputerCourses
Join training +91 90532 10052

Page Experience & Core Web Vitals

Digital Marketing & SEO · Lesson 9 of 30 Beginner 7 min read Updated

A visitor taps your ad, waits, sees a blank screen, then finally sees the page, and just as she taps "Book now" a banner loads and pushes the button down, so she taps the wrong thing. She leaves. Every part of that bad experience can be measured, and Google's name for the main measurements is Core Web Vitals.

In this lesson you will learn the three Core Web Vitals, their "good" thresholds, the difference between field and lab data, the tools that measure them, and the most common fixes. You will also watch a layout shift being measured live.

The three Core Web Vitals

MetricMeasuresGoodPoor
LCP: Largest Contentful PaintLoading: how long until the biggest text block or image in view appears2.5 seconds or lessMore than 4 seconds
INP: Interaction to Next PaintResponsiveness: how quickly the page reacts visually after taps, clicks and key presses200 milliseconds or lessMore than 500 milliseconds
CLS: Cumulative Layout ShiftVisual stability: how much visible content jumps around unexpectedly0.1 or lessMore than 0.25

Values in between are labelled "needs improvement". A page passes when it reaches "good" for all three at the 75th percentile of real page loads, measured separately for mobile and desktop. Using the 75th percentile means most visits, not only the lucky fast ones, must be good.

Core Web Vitals and ranking

Google recommends that site owners achieve good Core Web Vitals and says they line up with what its core ranking systems seek to reward. But they are one part of a wider picture called page experience, which also includes HTTPS, working well on mobile and not covering the content with intrusive pop-ups. Google is clear that relevance and helpful content still matter most: a fast page with weak content will not beat a slightly slower page that answers the question far better. Treat good vitals as a basic quality standard that helps visitors and conversions, not as a ranking shortcut.

Field data and lab data

  • Field data comes from real Chrome users visiting your pages, collected in the Chrome User Experience Report (CrUX). It is what Google uses to judge Core Web Vitals. Pages with little traffic may not have enough field data.
  • Lab data comes from a test run in a controlled setting, such as Lighthouse. It is useful for debugging because you can repeat it, but it is a simulation. Lighthouse cannot measure INP directly; it shows Total Blocking Time (TBT) as a lab stand-in.

Tools you can use free:

  • PageSpeed Insights shows field data (when available) and a Lighthouse lab test for any URL.
  • Search Console's Core Web Vitals report groups your site's URLs into good, needs improvement and poor, based on field data.
  • Chrome DevTools (the Performance and Lighthouse panels) for debugging on your own computer.

Watch a layout shift happen

This page shows a banner one second after loading, which pushes the button down. The script uses the browser's PerformanceObserver to add up layout shifts and shows the score. Run it, then change RESERVE_SPACE to true and run again.

HTMLTry it yourself
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Layout shift demo</title>
  <style>
    body { font-family: system-ui, sans-serif; margin: 1.5rem; max-width: 30rem; }
    #slot { background: #fff7ed; }
    .banner { padding: 1.5rem; background: #ea580c; color: #fff; }
    button { padding: .7rem 1.2rem; font-size: 1rem; }
    #score { margin-top: 1rem; font-weight: bold; }
  </style>
</head>
<body>
  <h1>Diwali sale</h1>
  <div id="slot"></div>
  <p>Gift boxes from ₹499. Free delivery in Jaipur.</p>
  <button type="button">Book now</button>
  <p id="score">CLS so far: 0</p>
  <script>
    const RESERVE_SPACE = false;
    const slot = document.getElementById("slot");
    if (RESERVE_SPACE) slot.style.minHeight = "4.5rem";

    let cls = 0;
    new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (!entry.hadRecentInput) cls += entry.value;
      }
      document.getElementById("score").textContent = "CLS so far: " + cls.toFixed(3);
    }).observe({ type: "layout-shift", buffered: true });

    setTimeout(() => {
      slot.innerHTML = '<div class="banner">Extra 10% off today only!</div>';
    }, 1000);
  </script>
</body>
</html>

Without reserved space, the banner pushes everything below it and the score rises above zero. With min-height reserving room in advance, the banner fills space that was already there, so nothing moves and the score stays at 0. The layout-shift API works in Chromium-based browsers such as Chrome and Edge; in other browsers the score may stay at 0.

Common fixes for each metric

LCP (loading):

  • Compress and resize images; serve modern formats such as WebP or AVIF.
  • Do not lazy-load the main image at the top of the page; you can mark it as important instead.
  • Use good hosting and caching, and remove heavy plugins and scripts you do not need.

INP (responsiveness):

  • Reduce heavy JavaScript, especially third-party scripts such as chat widgets and multiple tracking tags.
  • Break long tasks into smaller pieces so the browser can respond to taps in between.

CLS (stability):

  • Always give images and videos width and height attributes so the browser reserves space.
  • Reserve space for banners, ads and embeds that load later.
  • Avoid inserting content above existing content after the page has loaded.
HTML
<!-- Main hero image: sized, not lazy, marked as high priority -->
<img src="diwali-boxes.webp" alt="Diwali gift boxes on a festive table" width="1200" height="675" fetchpriority="high">

<!-- Images further down: sized and lazy-loaded -->
<img src="kitchen.webp" alt="Our sweet kitchen in Jaipur" width="800" height="500" loading="lazy">

Checking the 75th percentile

Suppose you collected LCP times (in seconds) from 12 real visits. This program finds the 75th percentile with a simple nearest-rank method and labels the result using Google's thresholds.

PythonRun it yourself
import math

lcp_seconds = [1.8, 2.1, 1.6, 3.9, 2.4, 2.2, 5.1, 1.9, 2.6, 2.0, 3.1, 1.7]

def percentile(values, p):
    ordered = sorted(values)
    rank = math.ceil(p / 100 * len(ordered))   # nearest-rank method
    return ordered[rank - 1]

def label_lcp(seconds):
    if seconds <= 2.5:
        return "good"
    if seconds <= 4.0:
        return "needs improvement"
    return "poor"

p75 = percentile(lcp_seconds, 75)
print("Median LCP:", percentile(lcp_seconds, 50), "s")
print("75th percentile LCP:", p75, "s ->", label_lcp(p75))
Output
Median LCP: 2.1 s
75th percentile LCP: 2.6 s -> needs improvement

The median (2.1 s) looks fine, but a quarter of visits are slower than 2.6 s, so the page needs work. That is exactly why Google uses the 75th percentile and not the average.

Common mistakes

  • Chasing a perfect Lighthouse score. Lab scores help with debugging; field data shows what real visitors get.
  • Testing only on office Wi-Fi and a fast laptop. Test on a mid-range phone with mobile data.
  • Adding every widget and tracking tag. Each third-party script adds weight and can hurt INP.
  • Lazy-loading the hero image. It delays the largest element and worsens LCP.

Key points

  • Core Web Vitals are LCP (loading), INP (responsiveness) and CLS (visual stability).
  • Good thresholds: LCP 2.5 s or less, INP 200 ms or less, CLS 0.1 or less, at the 75th percentile of visits.
  • INP replaced FID in 2024.
  • Field data (CrUX, Search Console, PageSpeed Insights) reflects real users; lab data (Lighthouse) helps debugging.
  • Good vitals support page experience but do not replace relevant, helpful content.

Exercise

Change the Python example to work with INP values in milliseconds: [120, 180, 90, 640, 210, 150, 170, 95, 300, 140, 160, 110]. Write a label_inp() function using the thresholds from this lesson (good up to 200 ms, poor above 500 ms) and print the 75th percentile with its label.

Show solution
Python
import math

inp_ms = [120, 180, 90, 640, 210, 150, 170, 95, 300, 140, 160, 110]

def percentile(values, p):
    ordered = sorted(values)
    return ordered[math.ceil(p / 100 * len(ordered)) - 1]

def label_inp(ms):
    if ms <= 200:
        return "good"
    if ms <= 500:
        return "needs improvement"
    return "poor"

p75 = percentile(inp_ms, 75)
print("75th percentile INP:", p75, "ms ->", label_inp(p75))
Output
75th percentile INP: 180 ms -> good

One very slow interaction (640 ms) does not decide the result, but if more than a quarter of visits were slow, the label would change.

Quick Quiz

1. Which metric measures visual stability?

2. What is the "good" threshold for INP?

3. Which metric did INP replace in 2024?

4. Which fix most directly reduces layout shift from images?

Learn Digital Marketing & SEO with a mentor

Free lessons get you started. Our Digital Marketing & SEO training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call