A practical guide to Core Web Vitals for busy teams
What Largest Contentful Paint: how long it takes for the biggest image or block of text on the screen to show up. Good is 2.5 seconds or less., Interaction to Next Paint: how quickly the page visibly responds when someone clicks, taps, or types. Good is 200 milliseconds or less., and Cumulative Layout Shift: how much the content jumps around while the page loads. Good is a score of 0.1 or less. actually measure, why your numbers don't match, and where to spend your time first.
Performance work can turn into a rabbit hole fast. There are dozens of metrics, a ton of tools, and endless advice about tiny optimizations. Sometimes it feels like searching for a needle in a haystack. Sometimes that haystack is on fire.
Core Web Vitals help cut through a lot of that noise. They focus on three questions every visitor is basically asking:
- Is it loading? Can I see the main content yet?
- Is it responding? When I tap or type, does something happen right away?
- Is it stable? Does the page stay put, or does it jump around while I'm reading?
If you get those three right, your site is going to feel fast. In this article I'll go over what each metric means, why your numbers don't always match up, and the fixes that usually make the biggest difference.
The three metrics in plain language
| Metric | What it measures | Good | Poor |
|---|---|---|---|
| LCP Largest Contentful Paint | How long until the largest visible image or text block renders | ≤ 2.5 s | > 4 s |
| INP Interaction to Next Paint | How quickly the page visually responds to clicks, taps, and key presses | ≤ 200 ms | > 500 ms |
| CLS Cumulative Layout Shift | How much visible content unexpectedly moves around | ≤ 0.1 | > 0.25 |
Google looks at these at the 75th percentile of real page loads, separately for mobile and desktop. That means most of your visitors need a good experience, including people on older phones and slow connections, not just someone on a fast laptop.
INP replaced First Input Delay (FID) as a Core Web Vital in 2024, and it's a lot stricter. FID only measured the delay before the first interaction started. INP looks at how long it takes the page to respond to interactions throughout the whole visit.
Lab data vs. field data
One of the most common questions I hear is, "Lighthouse says we're fine, so why is Search Console saying we're failing?" It's because they're measuring different things.
- Lab data (Lighthouse, WebPageTest, Chrome DevTools) runs one simulated page load on one device profile. It's repeatable and great for debugging, but it isn't your users.
- Field data (the Chrome UX Report, Search Console, or your own real-user monitoring) comes from actual visits. It reflects real devices, networks, cache states, logged-in pages, and user behavior.
Field data is the scorecard. Lab data is what you use to figure out what's going on. Lighthouse can't really measure INP during a normal page load because nobody is clicking anything. So if your field INP is bad, you'll need to recreate the slow interactions yourself using the Performance panel in Chrome DevTools with CPU throttling turned on.
Optimize for field data. Use lab tools to understand why the field numbers look the way they do.
Fixing slow LCP
Your LCP element is usually a hero image, a big heading, or a banner. When LCP is slow, it almost always comes down to one of four things: the server is slow to respond, the browser finds the LCP resource too late, the resource takes too long to download, or something is blocking it from rendering once it arrives.
Make the LCP image discoverable and prioritized
If your LCP image is set with a CSS background-image or added by JavaScript, the browser won't find it until much later. Use a regular <img> in the HTML and let the browser know it's important:
<img
src="/img/hero-1200.jpg"
srcset="/img/hero-600.jpg 600w, /img/hero-1200.jpg 1200w"
sizes="100vw"
width="1200" height="600"
alt="Team reviewing a website report"
fetchpriority="high">
Never lazy-load the LCP image
Adding loading="lazy" to every image is a really common "optimization" that actually makes LCP worse. Lazy-load the images further down the page, but let the hero image load right away.
Right-size and compress
Use modern formats like AVIF or WebP, and use srcset so phones aren't downloading images sized for a big desktop monitor. If your hero image is 2 MB, it's going to hurt your LCP on mobile no matter what else you do.
Reduce render-blocking resources
Big CSS files and scripts in the <head> that aren't deferred can hold up the first render. Load scripts that aren't critical with defer or async, get rid of CSS you're not using, and be careful with third-party tags that load before your own content.
Fixing poor INP
INP problems are almost always JavaScript problems. When the main thread is busy with a long task, the browser can't respond to someone's click until that task is done.
Break up long tasks
Any task that takes over 50 ms counts as a "long task." If an event handler has a lot of work to do, update the UI first, then give control back to the browser before doing the rest:
button.addEventListener('click', async () => {
showSpinner(); // update the UI first
await yieldToMain(); // let the browser paint
await doExpensiveWork(); // then do the heavy lifting
});
function yieldToMain() {
if ('scheduler' in window && 'yield' in scheduler) {
return scheduler.yield();
}
return new Promise((resolve) => setTimeout(resolve, 0));
}
Audit third-party scripts
Analytics, tag managers, chat widgets, A/B testing tools, and ad scripts are all fighting over the same main thread. Make a list of every third-party script, what it does, and who on your team owns it. It's pretty common to find scripts nobody even remembers adding.
Ship less JavaScript
Code splitting, removing dependencies you don't need, and not rendering mostly static content on the client all cut down on how much work the main thread has to do. This matters most on mid-range Android phones, which is where most INP problems show up.
Fixing layout shift
CLS is probably the most annoying one for users. You go to tap a link, an ad loads, and suddenly you've tapped something else. The good news is it's usually the easiest one to fix.
- Always set image and video dimensions. Add
widthandheightattributes (or CSSaspect-ratio) so the browser reserves space before the file loads. - Reserve space for dynamic content. Ads, embeds, cookie banners, and "you might also like" sections should have a fixed minimum height, or appear in overlays that don't push content.
- Tame web fonts. A fallback font with very different metrics causes text to reflow when the web font arrives. Use
font-display: swaptogether with fallback metric overrides likesize-adjust, or preload your most important font file. - Animate with transforms. Animating
top,height, ormarginmoves surrounding content;transformdoesn't.
Where to start
If you only have a few hours this sprint, this is the order I'd go in:
- Check field data first. Look at Search Console's Core Web Vitals report or PageSpeed Insights for your key templates. Find out which metric is failing, and on which device type.
- Focus on your highest-value templates. Home page, product or service pages, and checkout or contact flows matter more than the blog archive.
- Fix the LCP element. Make it discoverable, prioritized, and correctly sized. This is often the single biggest win.
- Add dimensions to every image. A quick sweep usually eliminates most layout shift.
- Inventory third-party scripts. Remove what you don't need and defer what you do.
Performance isn't a one-and-done project. New features, new marketing tags, and new images will slowly eat away at your improvements. Set a performance budget in your build, check your field data every month, and treat slowdowns like bugs.
Not sure what's slowing your site down?
My performance audit looks at your Core Web Vitals using both real user data and my own testing. Your team gets a plan with every fix ranked by how much your users will actually notice it.
Request a performance audit