Web Performance: Core Web Vitals Guide
A practical guide to Core Web Vitals: LCP, INP, and CLS. How they are measured, what hurts them, and concrete fixes that move the numbers.
What you'll learn
- ✓What LCP, INP, and CLS measure
- ✓How field data differs from lab data
- ✓Concrete causes for each metric
- ✓Practical fixes that move the numbers
- ✓How to monitor changes over time
Prerequisites
- •Comfortable with HTML, CSS, JS
Core Web Vitals are Google’s attempt to compress “how fast does this site feel?” into three measurable numbers. They are not the whole story of performance, but they are the part Google’s ranking signal cares about and the part users notice. This guide explains what each one measures, what tends to hurt it, and what you can do about it without rewriting your stack.
What and why
There are three current Core Web Vitals:
- LCP (Largest Contentful Paint) measures load speed. It is the time from navigation to when the largest visible element (image, video poster, or text block) is rendered. Good: 2.5s or less.
- INP (Interaction to Next Paint) measures responsiveness. It is the longest delay between any user interaction (click, tap, key) and the next visual update during the page’s lifetime. Good: 200ms or less. INP replaced FID in 2024.
- CLS (Cumulative Layout Shift) measures visual stability. It sums up unexpected layout shifts during the page’s life. Good: 0.1 or less.
Google measures these in the field via the Chrome User Experience Report (CrUX), which aggregates anonymous data from real Chrome users. Lab tools like Lighthouse give you a snapshot, but field data is what shows up in search and what users actually experience.
Mental model
Each metric maps to a phase of the user’s experience.
Navigation start
|
v
[ Load ] <- LCP: when does the main content appear?
|
v
[ Idle ] <- waiting for user
|
v
[ Interact ] <- INP: how snappy is each interaction?
|
v
[ Lifetime ] <- CLS: how much did things shift unexpectedly? Fixing LCP
LCP is usually an image or a hero text block. The fixes follow the request chain.
- Serve the LCP element from cache or CDN. A slow origin makes everything worse.
- Preload it.
<link rel="preload" as="image" href="hero.jpg" fetchpriority="high">for the hero image. - Avoid lazy-loading the LCP image.
loading="lazy"on the hero is a self-inflicted wound. - Use modern formats. AVIF or WebP at appropriate resolutions.
- Trim render-blocking resources. Defer non-critical CSS and JS so the browser can paint sooner.
<link rel="preload" as="image" href="/hero.avif" fetchpriority="high" />
<img src="/hero.avif" width="1200" height="630" alt="" />
If the LCP is text, ensure the font does not delay it. Use font-display: swap and preload critical fonts.
Fixing INP
INP is about main-thread availability when the user interacts. Long JavaScript tasks during interaction are the usual culprit.
button.addEventListener('click', () => {
// bad: 300ms of synchronous work blocks the next paint
for (const item of bigArray) heavyComputation(item);
});
Break long tasks into chunks and yield:
async function chunked(items) {
for (let i = 0; i < items.length; i++) {
heavyComputation(items[i]);
if (i % 50 === 0) await new Promise(r => setTimeout(r));
}
}
Other levers:
- Move heavy computation to a Web Worker.
- Avoid synchronous third-party scripts during interaction.
- Use
requestIdleCallbackfor non-urgent work after the response. - In React, wrap non-urgent state updates in
startTransitionso the response paints first.
Fixing CLS
CLS is the easiest to diagnose. Open DevTools, navigate the page, and watch elements jump.
The repeat offenders:
- Images without dimensions. Always set
widthandheight(or CSSaspect-ratio). - Ads and embeds that load late. Reserve space with a placeholder of known size.
- Web fonts swapping after load. Use
size-adjustand matching fallback metrics to keep layout stable. - Dynamic content inserted above the fold. Reserve space, or render at the bottom.
<img src="cover.jpg" width="800" height="450" alt="" />
<style>
.ad-slot { aspect-ratio: 300 / 250; background: #eee; }
</style>
Measuring it yourself
The web-vitals library reports the three metrics from real users.
import { onLCP, onINP, onCLS } from 'web-vitals';
onLCP((m) => sendToAnalytics(m));
onINP((m) => sendToAnalytics(m));
onCLS((m) => sendToAnalytics(m));
Send these to your analytics endpoint. Aggregate by page template, device, and country. Real field data tells you where to invest, while Lighthouse helps debug individual pages in the lab.
Common pitfalls
- Optimizing only on a fast laptop. Mid-range Android phones on slow networks are where the metrics fail.
- Treating Lighthouse scores as ground truth. They are a useful indicator, not the same as field data.
- Adding a service worker and assuming everything is now fast. Cold loads still happen, and slow caching strategies can make things worse.
- Fixing CLS by hiding the shifting element. The shift still occurs; you just stopped seeing it.
Best practices
- Set a performance budget per page template and enforce it in CI with Lighthouse CI or Calibre.
- Track INP regressions by deploy. A new third-party script is the most common culprit.
- Audit third parties quarterly. They are responsible for a disproportionate share of bad metrics.
- Optimize the LCP element specifically. The rest of the page can wait; the LCP cannot.
FAQ
Why did INP replace FID? FID only measured the first input delay. INP captures interactions throughout the session, which better matches what users feel.
Are the thresholds the same on mobile and desktop? Yes, the thresholds are device-agnostic, but mobile field data tends to be worse, so it dominates the score.
Do single-page apps get worse Core Web Vitals? Initial load is similar, but client-side route transitions can produce surprising CLS and INP. Frameworks now measure soft navigations.
Does fixing these improve SEO? They are a confirmed ranking signal, modest but real. The bigger payoff is user retention.
Related articles
- Web Content Security Policy Explained
A practical tour of CSP: what each directive does, how nonces and hashes work, how to roll out a policy safely with report-only mode, and the mistakes that quietly weaken it.
- Web Cookies vs localStorage vs sessionStorage
Compare the three main browser storage mechanisms by lifecycle, capacity, security, and use case so you pick the right tool for tokens, preferences, and state.
- Web CORS Explained In Depth
Understand Cross-Origin Resource Sharing: simple vs preflight requests, credentials, and the headers that decide whether your browser will let your fetch succeed.
- Web JWT vs Session Authentication
Compare JSON Web Tokens with classic server-side sessions across storage, revocation, scaling, and security so you can choose the right model for your app.