Last month, a client showed me their Google Search Console. Red arrows everywhere. “Core Web Vitals” — the phrase every developer dreads. Their mobile LCP was 6.2 seconds. INP was 580ms. CLS was 0.35. Google was actively penalizing them.
Three weeks later, all three metrics were green. LCP dropped to 1.8s. INP to 120ms. CLS to 0.02. Their organic traffic jumped 34% the following month.
Here’s the exact playbook I used — not theory, but the actual fixes that moved the needle.
What Are Core Web Vitals in 2026?
Google measures three things. That’s it. Not 20 metrics. Not a dozen tools. Three numbers that determine whether your site ranks or dies.
LCP (Largest Contentful Paint) — how fast your main content loads. Target: under 2.5 seconds. This is the hero image, the headline, the thing your user came for.
INP (Interaction to Next Paint) — how fast your page responds when clicked. Target: under 200 milliseconds. This replaced FID in March 2024, and it’s the metric that trips up most developers.
CLS (Cumulative Layout Shift) — how stable your page is while loading. Target: under 0.1. This is the “why did the button move when I tried to click it” metric.
Here’s what most people get wrong: they try to optimize all three at once. Don’t. Fix them in order — LCP first, INP second, CLS last. Each one has its own bottleneck, and solving them in the right order saves hours.
LCP: Fixing the Loading Problem
LCP is almost always one of three things: a slow server, a big image, or render-blocking JavaScript. Here’s how I diagnosed and fixed each one.
The Server Problem
The rule: Your server should respond in under 200ms (Time to First Byte). If TTFB is over 800ms, no amount of frontend optimization will save your LCP.
On one project, the LCP was 5.8 seconds. The TTFB alone was 2.1 seconds. The site was on a shared host with no caching. Here’s what I did:
- Moved to Cloudflare Pages — static hosting with edge caching. TTFB dropped from 2.1s to 45ms.
- Added stale-while-revalidate headers — users get cached content instantly while the background updates.
- Preconnected to critical origins —
<link rel="preconnect" href="https://fonts.googleapis.com" />saves 200-400ms on font loading.
The LCP dropped from 5.8s to 1.9s. Just from server-side changes. No frontend code touched.
The Image Problem
This is the most common LCP killer. Your hero image is 2MB, unoptimized, and loading after JavaScript parses.
Here’s the fix I use on every project:
<link rel="preload" as="image" href="/hero.webp" />
<img src="/hero.webp" alt="Hero" width="1200" height="600" fetchpriority="high" />
Two things matter here. fetchpriority="high" tells the browser: “this image is more important than everything else.” And width/height prevent layout shifts (which helps CLS too — two birds).
| Optimization | LCP Impact |
|---|---|
| Convert to WebP/AVIF | 40-70% smaller |
| Add fetchpriority=“high” | 0.5-1.5s faster |
| Preload LCP image | 0.3-0.8s faster |
| Set width/height attributes | Prevents CLS penalty |
| Edge caching (Cloudflare) | TTFB under 100ms |
The JavaScript Problem
If your JavaScript blocks rendering, the browser can’t paint anything until it downloads and parses every script. I’ve seen sites load 400KB of JavaScript before the first pixel appears.
The fix is simple but brutal: defer everything that isn’t critical.
<!-- Bad: blocks rendering -->
<script src="/app.js"></script>
<!-- Good: downloads in parallel, executes after paint -->
<script src="/app.js" defer></script>
For Astro sites specifically, the framework handles this automatically. That’s one reason I migrated this blog from React to Astro — the LCP went from 2.8s to 0.9s without any manual optimization.
INP: The Responsiveness Problem Nobody Talks About
INP is the hardest metric to fix because it’s not about loading — it’s about what happens after the page loads. Every click, every tap, every keystroke is measured.
“INP measures the full lifecycle of an interaction — from the moment you click to the moment the browser paints the result. If your click handler takes 500ms of JavaScript work, your INP is 500ms.” — Google Web Dev Blog (as of March 2024)
The Long Task Problem
The #1 cause of bad INP is long tasks — JavaScript functions that run for more than 50ms without yielding back to the browser. If your click handler runs for 300ms, the user feels a 300ms delay.
Here’s how I found and fixed them:
- Open Chrome DevTools → Performance tab
- Record a user interaction (click a button, open a menu)
- Look for red bars in the Main thread — those are long tasks
- Click on each one to see which function is responsible
On a recent project, a single analytics script was blocking the main thread for 280ms on every click. The fix?
<!-- Bad: loads on every page, blocks main thread -->
<script src="https://analytics.example.com/tracker.js"></script>
<!-- Good: loads after page is interactive -->
<script>
window.addEventListener('load', () => {
const script = document.createElement('script');
script.src = 'https://analytics.example.com/tracker.js';
script.async = true;
document.head.appendChild(script);
});
</script>
The INP dropped from 420ms to 95ms. One script. One change.
The Event Handler Problem
Here’s something most developers don’t realize: every event listener you add is a potential INP killer.
If you have a scroll listener that runs on every pixel of scroll, or a resize listener that recalculates layout on every frame, you’re tanking your INP.
The fix: use requestAnimationFrame for visual updates, and debounce everything else.
// Bad: runs on every scroll event (hundreds of times per second)
window.addEventListener('scroll', () => {
updateHeader();
updateProgressBar();
checkLazyImages();
});
// Good: runs at most once per frame (~60 times per second max)
let ticking = false;
window.addEventListener('scroll', () => {
if (!ticking) {
requestAnimationFrame(() => {
updateHeader();
updateProgressBar();
ticking = false;
});
ticking = true;
}
});
Third-Party Scripts: The Silent INP Killer
This is the part that makes developers angry. Your code is fast. Your INP is terrible. Why? Because that chat widget, that ad network, that heatmap tracker — they’re all running on the main thread.
As of early 2026, 43% of websites fail the INP threshold, and third-party scripts are the #1 cause.
Here’s what I do:
requestIdleCallback for non-critical scriptsCLS: The Visual Stability Problem
CLS is the easiest metric to fix — and the one developers ignore the most. It measures how much your page moves around while loading. Every unexpected shift adds to your CLS score.
The rule: Reserve space for everything before it loads. If an element’s size is unknown, give it a minimum height.
The Image Problem (Again)
I mentioned this under LCP, but it matters for CLS too. Images without width and height attributes cause layout shifts every single time.
<!-- Bad: browser doesn't know the size, page shifts when image loads -->
<img src="/photo.jpg" alt="Photo" />
<!-- Good: browser reserves the exact space -->
<img src="/photo.jpg" alt="Photo" width="800" height="600" />
The Font Swap Problem
Web fonts cause a specific type of layout shift: the text renders in a fallback font, then jumps when the web font loads. This is called FOUT (Flash of Unstyled Text).
The fix: use font-display: swap and set a good fallback font.
@font-face {
font-family: 'Inter';
src: url('/fonts/inter.woff2') format('woff2');
font-display: swap;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
This way, the fallback font is close enough to the web font that the shift is barely noticeable.
“The best CLS fix is the one you never think about: always set explicit dimensions. Always. For images, videos, iframes, ads, embeds — everything.”
The Ad and Embed Problem
Ads and embedded content (YouTube videos, tweets, maps) are notorious CLS offenders. They load asynchronously and push content around.
The solution: reserve a container with a fixed aspect ratio.
<div style="aspect-ratio: 16/9; background: #f0f0f0;">
<iframe src="https://youtube.com/embed/..." loading="lazy"></iframe>
</div>
The gray box appears immediately, reserving the exact space. When the video loads, nothing shifts.
The Results: Before and After
Here are the actual numbers from the project I mentioned at the start:
| Metric | Before | After | Target |
|---|---|---|---|
| LCP | 6.2s | 1.8s | ≤ 2.5s |
| INP | 580ms | 120ms | ≤ 200ms |
| CLS | 0.35 | 0.02 | ≤ 0.1 |
The organic traffic increase of 34% came two weeks after all three metrics went green. That’s not a coincidence — Google confirmed that Core Web Vitals are a ranking factor (as of May 2026).
My Core Web Vitals Checklist
If you only remember one thing from this article, make it this checklist. Run through it on every project:
One side note: Don’t obsess over perfect scores. A “good” LCP of 2.4s is fine. You don’t need 0.8s. The diminishing returns are real. Focus on getting all three metrics to “good,” then move on to building features.
The Bottom Line
Core Web Vitals aren’t about perfection. They’re about not being terrible. Most sites fail because of a handful of obvious mistakes: unoptimized images, render-blocking JavaScript, and third-party scripts running wild.
Fix those three things, and you’ll pass. It took me three weeks on that client project, but most of that time was waiting for server migrations and testing. The actual code changes took about two days.
Bookmark this one. Next time Google Search Console sends you a Core Web Vitals warning, you’ll know exactly what to do.