Page Experience & Core Web Vitals
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
| Metric | Measures | Good | Poor |
|---|---|---|---|
| LCP: Largest Contentful Paint | Loading: how long until the biggest text block or image in view appears | 2.5 seconds or less | More than 4 seconds |
| INP: Interaction to Next Paint | Responsiveness: how quickly the page reacts visually after taps, clicks and key presses | 200 milliseconds or less | More than 500 milliseconds |
| CLS: Cumulative Layout Shift | Visual stability: how much visible content jumps around unexpectedly | 0.1 or less | More 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.
<!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
widthandheightattributes 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.
<!-- 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.
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))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
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))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?
Cumulative Layout Shift measures how much visible content moves unexpectedly.
2. What is the "good" threshold for INP?
INP should be 200 ms or less at the 75th percentile.
3. Which metric did INP replace in 2024?
INP replaced FID because it measures responsiveness across the whole visit.
4. Which fix most directly reduces layout shift from images?
Width and height let the browser reserve space before the image loads.
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
