Core Web Vitals Optimization 2026: Complete Performance Guide
A web performance engineering guide to diagnosing and fixing Core Web Vitals. Covers what LCP, INP, and CLS actually measure, the specific things that cause each metric to fail, how to read field data in Search Console, and how to verify that your fixes actually changed user behavior.
On this page
What Core Web Vitals Actually Measure
Core Web Vitals are three metrics that Google uses as ranking signals. They are collected from real Chrome users visiting your site, aggregated over a rolling 28-day window, and evaluated at the 75th percentile. That last part matters: if 74 out of 100 visits have a good LCP but the 75th is slow, you fail. The three metrics each capture a different dimension of user experience: loading, responsiveness, and visual stability.
If you want to run your own numbers, our Core Web Vitals calculator lets you plug in your scores and see exactly where you stand against each threshold.
Largest Contentful Paint (LCP)
LCP measures how long it takes for the largest visible element in the viewport to finish rendering. That element is usually a hero image, a background image applied via CSS, or a large block of text. The browser determines the LCP element dynamically as the page loads; it can change as new, larger elements appear. The final LCP candidate is the one reported.
The threshold is 2.5 seconds. Anything between 2.5 and 4.0 seconds needs improvement. Above 4.0 seconds is poor. On most sites we audit through our SEO audit service, LCP is the metric that fails most often, and the root cause is almost always discoverable in under ten minutes with the right tools.
Interaction to Next Paint (INP)
INP replaced First Input Delay in March 2024 and is a far more demanding metric. Where FID only measured the delay before the browser started processing the very first interaction, INP measures the full latency of every interaction on the page (click, tap, keypress) and reports the worst one at the 98th percentile. That means a page that responds well to 97 out of 100 interactions but chokes on 3 of them still gets a bad INP score.
The threshold is 200 milliseconds. The measurement includes three phases: the input delay (time spent waiting for existing tasks to finish), the processing time (time spent running your event handlers), and the presentation delay (time from when your handlers finish to when the browser paints the next frame). All three phases contribute to the total, and fixing INP often requires addressing all three.
Cumulative Layout Shift (CLS)
CLS quantifies how much visible content shifts around unexpectedly during the page lifecycle. It uses a session window approach: layout shifts that occur within 1 second of each other and no more than 5 seconds total are grouped into a session window, and CLS reports the largest session window score. A shift only counts if it was not preceded by a user interaction within 500 milliseconds, so tapping a button that reveals a dropdown does not penalize you.
The threshold is 0.1. CLS is a unitless score calculated by multiplying the fraction of the viewport that shifted by the distance the content moved. A score of 0.1 is roughly equivalent to an element that covers 25% of the viewport shifting by 40% of the viewport height. Beyond that, users notice and get frustrated.
What Causes LCP to Fail
LCP failures come from four categories: slow server response, render-blocking resources, slow resource load times, and client-side rendering. On most sites, the problem is a combination of two or three of these, not just one.
Hero images without preload hints
The browser discovers resources by parsing HTML. If your LCP element is an image referenced in CSS (a background-image, for instance) or loaded via JavaScript, the browser cannot find it until it has downloaded, parsed, and executed that CSS or JS. By then, you have already lost hundreds of milliseconds. The fix is a preload link in the document head that tells the browser about the resource before it would normally discover it:
<head> <link rel="preload" as="image" href="/hero.webp" fetchpriority="high"> </head>
The fetchpriority="high" attribute is critical. Without it, the browser may still deprioritize the image behind stylesheets and scripts. On the image element itself, make sure you also set fetchpriority="high" and loading="eager" (never lazy-load your LCP image):
<img src="/hero.webp" alt="Descriptive alt text" width="1200" height="630" fetchpriority="high" loading="eager" >
Render-blocking CSS
Every stylesheet in your document head blocks rendering until it finishes downloading and parsing. If you have three CSS files totaling 200KB, the browser will not paint a single pixel until all three are ready. The most effective fix is to inline the critical CSS needed for the above-the-fold content directly in a style tag and defer the rest:
<head>
<!-- Inline critical CSS -->
<style>
/* Only styles needed for above-the-fold content */
.hero { display: flex; align-items: center; min-height: 60vh; }
.hero img { width: 100%; height: auto; }
</style>
<!-- Defer non-critical CSS -->
<link rel="preload" href="/styles.css" as="style"
onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/styles.css"></noscript>
</head>Slow TTFB
Time to First Byte is the foundation of LCP. If the server takes 800 milliseconds to respond, your LCP cannot be faster than 800 milliseconds plus the time to download and render the resource. Common causes include uncached database queries, server-side rendering without caching, and geographic distance between the user and the server. A CDN with edge caching solves the geographic problem. For dynamic content, stale-while-revalidate caching lets you serve a cached response immediately while regenerating in the background:
Cache-Control: public, max-age=60, stale-while-revalidate=3600
You can measure TTFB in PageSpeed Insights under the server response time audit, or in WebPageTest waterfall view, where you can see exactly how much time the browser spent waiting for the server versus downloading the response.
Client-side rendering delays
Single-page applications that render everything in JavaScript add a significant delay to LCP. The browser must download the HTML, then download the JS bundle, then execute it, and only then does the LCP element appear in the DOM. Server-side rendering (SSR) or static site generation (SSG) eliminates this by putting the LCP element in the initial HTML response. If you are on Next.js, React Server Components handle this automatically for any component that does not use "use client".
What Causes INP to Fail
INP failures mean the browser is unable to respond to user interactions within 200 milliseconds. The three main culprits are heavy JavaScript event handlers, long tasks that block the main thread, and third-party scripts competing for CPU time. For a deeper walkthrough of INP-specific fixes, see our guide on how to fix Core Web Vitals issues.
Heavy JavaScript event handlers
When a user clicks a button and your click handler kicks off a synchronous operation that takes 300 milliseconds, the browser cannot paint an update until the handler finishes. The fix is to break expensive work out of the event handler and yield back to the browser so it can paint. The scheduler.yield() API (available in Chrome and progressively adopted by other browsers) is the cleanest way to do this:
button.addEventListener('click', async () => {
// Do the minimum work needed for visual feedback
button.textContent = 'Processing...';
button.disabled = true;
// Yield to let the browser paint that update
await scheduler.yield();
// Now do the expensive work
const result = processData(largeDataset);
renderResults(result);
});The key insight is that the browser measures INP from the moment of interaction to the next paint. If you update the UI immediately (changing button text, showing a spinner) and then yield, the INP measurement captures that fast initial paint rather than the entire processing time.
Long tasks blocking the main thread
A long task is any JavaScript task that runs for more than 50 milliseconds. During a long task, the browser cannot process user input. If a user clicks while a long task is running, the input delay phase of INP grows by however long the task has left to run. You can identify long tasks in Chrome DevTools by opening the Performance panel, recording an interaction, and looking for tasks with red corners in the flame chart. Those red corners indicate the task exceeded 50 milliseconds.
The fix is to break long tasks into smaller chunks. For loops that iterate over large arrays, process items in batches with yields between them:
async function processInChunks(items, chunkSize, processFn) {
for (let i = 0; i < items.length; i += chunkSize) {
const chunk = items.slice(i, i + chunkSize);
chunk.forEach(processFn);
// Yield between chunks so the browser stays responsive
if (i + chunkSize < items.length) {
await scheduler.yield();
}
}
}Third-party scripts
Analytics, chat widgets, ad scripts, and consent management platforms all compete for main thread time. The worst offenders are scripts that synchronously inject other scripts, creating chains of blocking work. Audit your third-party scripts by loading your page in Lighthouse and looking at the "Reduce the impact of third-party code" audit. For scripts that are not needed at page load, defer them until after user interaction:
// Load chat widget only after first user interaction
const events = ['click', 'scroll', 'keydown', 'touchstart'];
function loadChat() {
const script = document.createElement('script');
script.src = 'https://chat-widget.example.com/loader.js';
document.body.appendChild(script);
events.forEach(e => document.removeEventListener(e, loadChat));
}
events.forEach(e => document.addEventListener(e, loadChat, { once: true }));This pattern ensures that the chat widget script is never downloaded, parsed, or executed until a user has actually engaged with the page. That keeps the main thread free during the initial load and improves both LCP and INP.
What Causes CLS to Fail
Layout shifts happen when visible elements move after they have already been painted to the screen. The three most common sources are images without explicit dimensions, dynamically injected content, and web font loading. Our companion guide covers additional CLS scenarios in detail: how to improve Core Web Vitals in 2026.
Images and videos without dimensions
When the browser encounters an image tag without width and height attributes, it allocates zero space for it. When the image finishes loading, the browser reflows the page to make room, pushing everything below it downward. The fix is straightforward: always set width and height on image elements. The browser uses these to calculate the aspect ratio and reserve the correct amount of space before the image loads.
<!-- BAD: no dimensions, causes layout shift -->
<img src="/photo.webp" alt="A descriptive caption">
<!-- GOOD: dimensions set, browser reserves space -->
<img src="/photo.webp" alt="A descriptive caption"
width="800" height="450" loading="lazy">
<!-- ALSO GOOD: CSS aspect-ratio for responsive images -->
<style>
.responsive-img {
width: 100%;
height: auto;
aspect-ratio: 16 / 9;
}
</style>
<img src="/photo.webp" alt="A descriptive caption"
class="responsive-img" loading="lazy">The same principle applies to video embeds, iframes, and ad slots. Any element whose final size is unknown at render time needs a container with explicit dimensions or an aspect-ratio declaration.
Dynamically injected content
Banners, cookie consent bars, notification prompts, and dynamically loaded content sections all cause CLS when they push existing content around. The worst offender is content injected above the current viewport. If a banner appears at the top of the page after the user has started reading, everything shifts down.
The fix depends on the element. For banners and alerts, use CSS transforms (translate) or fixed/sticky positioning so they overlay content instead of displacing it. For dynamically loaded sections, reserve the expected height with a min-height on the container. For ad slots, always set explicit dimensions on the container div, even before the ad loads.
Web font loading
When a custom web font loads and replaces the fallback font, the different glyph widths and heights cause text to reflow. This is often subtle but can produce measurable CLS, especially on text-heavy pages. The foundational fix is font-display: swap combined with preloading the font file:
<link rel="preload" href="/fonts/custom-font.woff2"
as="font" type="font/woff2" crossorigin>
<style>
@font-face {
font-family: 'CustomFont';
src: url('/fonts/custom-font.woff2') format('woff2');
font-display: swap;
}
/* Override fallback font metrics to match custom font */
@font-face {
font-family: 'CustomFont-Fallback';
src: local('Arial');
ascent-override: 90%;
descent-override: 22%;
line-gap-override: 0%;
size-adjust: 105%;
}
body {
font-family: 'CustomFont', 'CustomFont-Fallback', sans-serif;
}
</style>The ascent-override, descent-override, line-gap-override, and size-adjust descriptors on the fallback font face let you tune the fallback metrics to match the custom font. When the swap happens, the text occupies nearly the same space, producing minimal or zero layout shift. You can calculate the right values using font metric tools or by measuring both fonts side by side in DevTools.
Diagnosing Issues with PageSpeed Insights and Lighthouse
The two primary diagnostic tools are PageSpeed Insights and Google Lighthouse. They serve different purposes, and understanding the distinction saves you from chasing the wrong numbers.
PageSpeed Insights shows two sections: field data and lab data. The field data at the top is what matters for rankings. It comes from real Chrome users who visited your page over the past 28 days. If not enough users visited a specific URL, PageSpeed Insights falls back to origin-level data (aggregated across your entire domain). The lab data below is a Lighthouse run performed from Google's servers at the time you request the test.
Lighthouse, whether run through PageSpeed Insights, Chrome DevTools, or the command line, produces lab data only. Lab data is useful for debugging because it gives you a detailed trace, a filmstrip, and specific recommendations tied to line numbers in your code. But lab data cannot measure INP because there are no real user interactions in a lab test. Lighthouse uses Total Blocking Time (TBT) as a proxy for responsiveness, but TBT and INP often diverge.
For a complete audit workflow, start with PageSpeed Insights to see whether your field data passes. If it does not, use the lab data diagnostics to identify specific issues. Then open the page in Chrome DevTools, go to the Performance panel, and record interactions manually to trace the exact source of INP problems. Our page speed analyzer can help you run these checks in bulk across your sitemap.
WebPageTest is another essential tool, particularly for LCP diagnosis. Its waterfall view shows every network request in chronological order, making it easy to spot resources that are discovered late. Run a test with the "filmstrip" option enabled and look at exactly which request corresponds to the LCP element. If that request starts late in the waterfall, you need a preload hint or need to move the resource reference earlier in the HTML.
The 28-Day Field Data Window in Search Console
Google Search Console has a dedicated Core Web Vitals report under the Experience section. This report groups your URLs into "Good," "Needs improvement," and "Poor" based on field data. It is the canonical source of truth for whether Google considers your pages to pass.
The data uses a rolling 28-day collection window. When you deploy a fix today, the next 28 days of data will gradually include sessions with the improved code. But the older data from before the fix is still in the window, diluting the improvement. Depending on your traffic volume, it typically takes four to six weeks for your improvements to fully materialize in the report. On low-traffic pages, it can take longer because there are fewer data points to replace the old ones.
A practical approach: after deploying fixes, monitor the PageSpeed Insights field data for the specific URL. If the URL-level data is not available (insufficient traffic), check the origin-level data. When you see the field numbers shift, you know the fix is being captured. Then wait for the Search Console report to reflect the change. Do not panic if the report takes a few weeks to update.
Bing Webmaster Tools also surfaces page experience signals, and while Bing uses a different ranking algorithm, the same fixes that improve your Core Web Vitals for Google also improve your performance metrics in Bing. Worth verifying after you deploy changes.
Correlating CWV Improvements with User Behavior Using Microsoft Clarity
Passing Core Web Vitals is not the end goal. The end goal is better user engagement and conversion. Microsoft Clarity is a free session recording and heatmap tool that lets you correlate performance improvements with actual user behavior changes.
After installing Clarity on your site, you can filter sessions by frustrated users (those who rage-clicked, excessive scrolled, or quickly navigated away). Before your CWV fixes, record the baseline rate of frustrated sessions. After the fixes propagate, compare. Clarity does not directly report Core Web Vitals, but the behavioral signals it captures are the downstream effects of the same problems: users clicking buttons that do not respond (INP), users scrolling past content that jumps around (CLS), and users abandoning pages that take too long to show content (LCP).
The heatmap feature is particularly useful for CLS diagnosis. If Clarity shows users repeatedly clicking in areas that are different from where your interactive elements actually render, that is a strong signal that layout shifts are moving buttons away from where users expect them. Session recordings let you watch this happen in real time on actual user devices.
For generating targeted performance fix specifications based on your Clarity data combined with Lighthouse audits, tools like Claude Code can help you translate diagnostic findings into specific code changes with the right context about your codebase.
A Complete LCP Fix: From Diagnosis to Verification
Here is an end-to-end walkthrough of diagnosing and fixing a common LCP issue. Start by entering your URL in PageSpeed Insights. Look at the field data section. If LCP is in the "Needs improvement" or "Poor" range, scroll down to the lab data and find the "Largest Contentful Paint element" diagnostic. This tells you exactly which DOM element was the LCP candidate.
Suppose the LCP element is a hero image. Open WebPageTest, run a test, and look at the waterfall. Find the request for that image. Check two things: when was the request initiated (relative to navigation start), and how long did it take to complete? If the request started late, you need a preload. If the request started early but took a long time, you need to optimize the image file size or your CDN configuration.
For a late-discovered hero image, the fix involves three changes. First, add a preload link in the document head. Second, ensure the image element has fetchpriority="high". Third, make sure no render-blocking resource is delaying the HTML parsing:
<!-- In document head -->
<link rel="preload" as="image" href="/hero.avif"
fetchpriority="high"
type="image/avif"
imagesrcset="/hero-400.avif 400w,
/hero-800.avif 800w,
/hero-1200.avif 1200w"
imagesizes="100vw">
<!-- In body -->
<img src="/hero-1200.avif"
srcset="/hero-400.avif 400w,
/hero-800.avif 800w,
/hero-1200.avif 1200w"
sizes="100vw"
alt="Descriptive alt text for the hero section"
width="1200" height="630"
fetchpriority="high"
loading="eager"
decoding="async">After deploying this fix, verify it in three steps. First, run PageSpeed Insights again and confirm the lab LCP improved. Second, open Chrome DevTools, go to the Network panel, and confirm the image request starts in the first few rows of the waterfall (meaning the preload worked). Third, wait 28 days and check the field data to confirm the improvement reached real users.
A Complete INP Fix: Yielding to the Browser
INP problems are harder to diagnose because they require real interactions. Open your page in Chrome DevTools and go to the Performance panel. Click "Record," then interact with the page the way a real user would: click buttons, type in form fields, toggle menus. Stop the recording and look for interactions flagged with red markers. Click on an interaction to see its total duration and the breakdown between input delay, processing time, and presentation delay.
Suppose you find that clicking the "Add to cart" button has an INP of 350 milliseconds, with 280 milliseconds in the processing phase. That means the click handler itself is the bottleneck. Open the handler and identify what it does synchronously. Common culprits include DOM manipulation across hundreds of elements, JSON parsing of large payloads, and analytics calls that run synchronously.
The fix is to restructure the handler to do the minimal visual update first, yield, and then do the heavy work:
// BEFORE: everything runs synchronously, INP = 350ms
addToCartBtn.addEventListener('click', () => {
const product = getProductDetails(); // 20ms
updateCartState(product); // 50ms
rerenderCartSidebar(); // 150ms
sendAnalyticsEvent('add_to_cart', product); // 60ms
});
// AFTER: visual update first, then yield, INP < 100ms
addToCartBtn.addEventListener('click', async () => {
const product = getProductDetails();
updateCartState(product);
// Show immediate feedback
cartBadge.textContent = getCartCount();
cartBadge.classList.add('animate-bounce');
// Yield so the browser paints the badge update
await scheduler.yield();
// Heavy work happens after the paint
rerenderCartSidebar();
sendAnalyticsEvent('add_to_cart', product);
});The INP measurement captures the time until the next paint after the interaction. By yielding after the visual feedback, you let the browser paint the cart badge update within 70 milliseconds instead of waiting 350 milliseconds for everything to finish.
Lab Data vs. Field Data: Understanding the Gap
A common source of confusion: your Lighthouse score says 95, but Search Console shows failing CWV. This happens because lab data and field data measure different things in different conditions.
Lighthouse runs on a single machine with a simulated throttled connection. It loads the page once, with no cached resources, and no real user interactions. Field data reflects the full range of your real audience: slow phones on 3G connections, users on older browsers, people with dozens of tabs open, users interacting with the page in ways you did not anticipate.
The 75th percentile threshold matters here. If 25% of your users are on slow devices, and those users experience LCP over 2.5 seconds, your field data will fail even though the median (50th percentile) might be excellent. This is why targeting faster thresholds in lab tests gives you a margin of safety. Aim for under 1.5 seconds LCP in Lighthouse to have confidence you will pass at the 75th percentile in the field.
INP is the metric where the lab-field gap is widest. Lighthouse cannot measure INP at all because there are no user interactions in a lab test. It reports TBT (Total Blocking Time) as a proxy, but TBT measures main thread blocking during load only, while INP measures interaction latency throughout the entire page lifecycle. A page can have excellent TBT and terrible INP if it loads fast but has heavy event handlers triggered by user actions after load.
Putting It All Together: An Optimization Workflow
Here is the workflow we follow when running technical SEO engagements for our clients. It works whether you are fixing one page or a thousand.
Start by pulling the Core Web Vitals report from Search Console. Export the list of "Poor" URLs. Group them by template: are they all product pages? All blog posts? All category pages? Template-level grouping lets you fix an entire class of pages with a single code change rather than fixing URLs one at a time.
For each failing template, pick one representative URL and run it through PageSpeed Insights and WebPageTest. Identify the LCP element, check for CLS sources in the filmstrip, and note the main thread blocking time. Then open the page in Chrome DevTools and manually interact with it while recording a Performance trace to find INP issues.
Prioritize fixes by impact. LCP and CLS fixes are usually straightforward (preload hints, image dimensions, font loading adjustments) and can be deployed in hours. INP fixes require more investigation but often come down to restructuring event handlers and deferring third-party scripts. After deploying, verify with lab tools immediately and monitor field data over the next four to six weeks.
Install Microsoft Clarity before you start making changes so you have a baseline of user behavior metrics. After your fixes propagate, compare the frustration rate, scroll depth, and engagement time. This gives you a concrete business case for the performance work beyond "the numbers turned green."
Ready to fix your Core Web Vitals?
We diagnose field data failures, implement fixes by template, and monitor the 28-day propagation window so you do not have to guess whether the changes landed.
Frequently Asked Questions
What are the three Core Web Vitals in 2026?
Largest Contentful Paint (LCP) with a threshold of 2.5 seconds, Interaction to Next Paint (INP) with a threshold of 200 milliseconds, and Cumulative Layout Shift (CLS) with a threshold of 0.1. These are evaluated at the 75th percentile of field data collected over a rolling 28-day window from real Chrome users.
How long does it take for improvements to show in Search Console?
The Search Console Core Web Vitals report uses a rolling 28-day window. After deploying fixes, new sessions with improved performance gradually replace older data. In practice, expect four to six weeks before the report fully reflects your changes. On low-traffic pages, it may take longer.
What replaced First Input Delay?
Interaction to Next Paint (INP) replaced FID in March 2024. Unlike FID, which only measured the delay before the browser processed the first interaction, INP measures the complete latency of every interaction throughout the page lifecycle and reports the worst interaction at the 98th percentile.
Why does my Lighthouse score look good but field data fails?
Lighthouse runs in controlled conditions on a single device. Field data reflects the full spectrum of your real users, including those on slow phones and congested networks. Additionally, field data evaluates at the 75th percentile, so if 25% or more of your users have poor experiences, you fail even if the majority is fine. Lighthouse also cannot measure INP since it has no real user interactions.
What is the most common cause of poor LCP?
A hero image that lacks a preload hint, causing the browser to discover it late in the loading process. Other common causes include render-blocking CSS, slow server response times, and client-side rendering that delays the LCP element from appearing in the DOM.
How do I fix CLS from web font loading?
Use font-display: swap in your @font-face declaration, preload the font file, and use the size-adjust, ascent-override, and descent-override CSS descriptors on your fallback font to match its metrics to the custom font. This minimizes the text reflow that occurs when the custom font replaces the fallback.