Expert · Fast, accessible, findable and safe: quality for real sites · topic 46 of 64 · 35 min
Performance and Core Web Vitals
Explanation
Many visitors in India use a budget Android phone on a crowded 4G network. A page that feels fine on your laptop and office Wi-Fi can take 8 seconds on that phone. People leave slow pages, shops lose orders, and Google uses page experience as one of many signals in search. Performance is not a luxury; it decides who can use your site.
The three Core Web Vitals
| Metric | What it measures | Good | Poor |
|---|---|---|---|
| LCP: Largest Contentful Paint | Time until the biggest image or text block in view is shown | 2.5 s or less | More than 4 s |
| INP: Interaction to Next Paint | Delay from a tap, click or key press until the screen updates (it looks at the slow interactions of the whole visit) | 200 ms or less | More than 500 ms |
| CLS: Cumulative Layout Shift | How much visible content jumps around without the user asking | 0.1 or less | More than 0.25 |
Between good and poor is "needs improvement". INP replaced the older FID (First Input Delay) as a Core Web Vital in March 2024: FID only measured the wait before the first tap, while INP judges responsiveness across the whole visit. A page passes when the 75th percentile of real visits is good, which means at least 75% of visits must be good. So the slow phones in your audience count, not just your fast laptop.
Lab data and field data
| Lab data | Field data | |
|---|---|---|
| Where it comes from | One test run in a controlled setting (Lighthouse) | Real visits by real Chrome users (the Chrome User Experience Report, CrUX) |
| Good for | Finding and fixing problems while you build | Knowing what users really feel |
| INP | Not measured; Lighthouse shows Total Blocking Time (TBT) as a hint | Measured |
Lighthouse is built into Chrome DevTools (the Lighthouse panel). PageSpeed Insights (pagespeed.web.dev) shows both: field data from CrUX at the top when your site has enough visitors, and a Lighthouse lab report below. Run Lighthouse in an Incognito window with "Mobile" selected, because browser extensions and a fast desktop hide problems.
Fix 1: images, often the biggest win
<!-- The hero image is usually the LCP element: load it early -->
<img src="hero-800.webp"
srcset="hero-400.webp 400w, hero-800.webp 800w, hero-1600.webp 1600w"
sizes="(max-width: 600px) 100vw, 800px"
width="800" height="450"
fetchpriority="high"
alt="Fresh vegetables at a Pune market">
<!-- Images further down the page: let the browser wait -->
<img src="stall.webp" width="400" height="300" loading="lazy" alt="Fruit stall">- Use modern formats: WebP works in every current browser, and AVIF is usually even smaller. A 2 MB phone photo can often become 100 KB or less.
- Serve the size you show: a phone screen 400 pixels wide does not need a 4000-pixel photo. srcset lists the files and their widths, sizes says how wide the image will be shown, and the browser picks the best file.
- Always write width and height (or set CSS aspect-ratio). The browser keeps the space ready, so text does not jump when the image arrives. This fixes most CLS problems.
- loading="lazy" delays images far down the page until the user scrolls near them.
- fetchpriority="high" tells the browser that the LCP image matters most.
Fix 2: less JavaScript, and short tasks
<!-- Blocks the page while it downloads and runs -->
<script src="app.js"></script>
<!-- Downloads in parallel, runs after the HTML is parsed, in order -->
<script src="app.js" defer></script>
<!-- Downloads in parallel, runs as soon as it is ready, in any order -->
<script src="analytics.js" async></script>Every kilobyte of JavaScript must be downloaded, parsed and run, and a budget phone runs it several times slower than a laptop. Remove libraries you do not need. Use code splitting: load a feature with a dynamic import() only when the user needs it, for example const { drawChart } = await import('./chart.js') inside a click handler. Vite and other bundlers create a separate file for each dynamic import.
// The main thread runs your JavaScript AND paints the screen.
// A task longer than 50 ms is a "long task": taps must wait, INP goes up.
function yieldToMain() {
return new Promise((resolve) => setTimeout(resolve, 0));
}
async function processOrders(orders) {
for (let i = 0; i < orders.length; i++) {
processOne(orders[i]);
if (i % 50 === 0) await yieldToMain(); // let the browser handle taps and paint
}
}- Caching: send long Cache-Control headers for files with a hash in their name (app.3f9a1c.js), so repeat visits load them from the phone instead of the network.
- CDN (Content Delivery Network): copies of your files on servers in many cities, so a user in Chennai downloads from a nearby server. Hosts like Netlify, Vercel and Cloudflare include one.
- Fonts: add font-display: swap to @font-face so text shows at once in a fallback font, and load only the weights you use. System fonts cost nothing.
- preconnect: <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> opens the connection to an important other server early.
- Compression: text files should be sent compressed with gzip or Brotli. Most hosts do this for you.
Watch the lesson
Performance and Core Web Vitals · 1/8
A slow page loses visitors, especially on budget phones and busy mobile networks.
Prefer a video by a teacher? Find free videos on “core web vitals LCP INP CLS explained lighthouse tutorial” on YouTube ↗
In real life: A grocery order page on a budget phone
A small grocery shop in Indore has an order page with a 3 MB banner photo and five chat and analytics scripts in the head. On a budget phone on 4G, the banner takes 7 seconds (poor LCP) and the "Add" buttons react slowly (poor INP). The developer converts the banner to a small WebP with fetchpriority="high", adds width and height to every product photo, lazy-loads the photos lower down, defers the scripts and removes two that nobody used. The page now passes Core Web Vitals and more people finish their orders.
Practice (0/3 done)
This vegetable shop page is slow and jumpy. Fix it: (1) the hero image (id="hero") must NOT have loading="lazy", must have fetchpriority="high", and must have width and height attributes; (2) every image inside .gallery must have loading="lazy" plus width and height attributes; (3) the <script src="app.js"> must have the defer attribute.
Write rateVital(name, value) that rates one Core Web Vital. name is "LCP" (value in milliseconds), "INP" (milliseconds) or "CLS" (a score with no unit). Return "good", "needs improvement" or "poor" using these limits: LCP good up to 2500, poor above 4000; INP good up to 200, poor above 500; CLS good up to 0.1, poor above 0.25. A value exactly on a limit belongs to the better band: rateVital("LCP", 2500) is "good" and rateVital("INP", 500) is "needs improvement".
Measure a real site. Open a site you built (or one you use often) in a Chrome Incognito window, run Lighthouse in DevTools with "Mobile" selected, then test the same URL on PageSpeed Insights (pagespeed.web.dev). Write down the numbers and your top three fixes.
Quick quiz
Read more (free)
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.