How to Optimize Core Web Vitals
Core Web Vitals are a set of metrics that Google uses to measure the real-world experience of visitors on your site. They were introduced as a ranking signal in 2021, and they remain one of the clearest proxies for page quality that search engines have access to. If your pages are slow to load, slow to respond, or jump around while rendering, you lose both rankings and revenue.
The three metrics that make up Core Web Vitals are LCP (Largest Contentful Paint), INP (Interaction to Next Paint), and CLS (Cumulative Layout Shift). Each one measures a different part of the loading and interaction experience. This guide explains what each metric means, how to measure it accurately, and the exact fixes that actually move the needle.
Understanding the Three Core Web Vitals
Before you can optimize anything, you need to know what you are optimizing. The three metrics measure distinct, non-overlapping concerns.
Largest Contentful Paint (LCP)
LCP measures the time from when the page starts loading until the largest visible element finishes rendering. That element is usually a hero image, a headline, or a large video poster. The browser reports LCP whenever a new largest element appears, so the final value is the render time of the biggest element that wins the race.
The thresholds are straightforward. A good LCP is 2.5 seconds or less, measured at the 75th percentile of page loads. Anything between 2.5 and 4.0 seconds needs improvement, and anything above 4.0 seconds is considered poor. Because LCP is measured at the 75th percentile, you are optimizing for the majority of real users, not for the fastest possible connection.
The four most common causes of a slow LCP are slow server response time, render-blocking resources, slow image loading, and slow rendering on the client. Each has a distinct fix, and the fixes compound: shaving a few hundred milliseconds off the server response helps every other metric as well.
Interaction to Next Paint (INP)
INP replaced First Input Delay (FID) as the interaction metric in March 2024. Unlike FID, which only measured the very first interaction, INP measures the latency of every interaction that happens during the page life, including clicks, taps, and key presses. The reported value is the worst interaction time that is not an outlier.
A good INP is 200 milliseconds or less. The metric is dominated by main-thread work: JavaScript that runs long tasks, heavy event handlers, and layout thrashing. The single most effective way to improve INP is to keep the main thread free, which usually means shipping less JavaScript and breaking up long tasks.
Cumulative Layout Shift (CLS)
CLS measures how much the page layout shifts during the entire lifecycle of the page, from initial load through scrolling and interaction. The score is the sum of all unexpected layout shifts, calculated as the shift distance times the fraction of the viewport that was affected.
A good CLS score is 0.1 or less. Layout shifts are almost always caused by content being injected into the page after the surrounding content has already been laid out. Images without explicit dimensions, dynamically injected ads, and fonts that load late are the classic offenders.
Measuring Core Web Vitals
You cannot fix what you cannot measure. Fortunately, measuring Core Web Vitals is easier than ever, and you should use more than one tool because each gives you a different view of the problem.
Field Data vs. Lab Data
Field data comes from real users through the Chrome UX Report (CrUX) and represents what actual visitors experience on real devices and connections. Lab data comes from a controlled environment such as Lighthouse or PageSpeed Insights and represents what the page does when loaded in a predictable way. You need both. Field data tells you whether you have a real problem; lab data tells you where to look for it.
Tools like Lighthouse and PageSpeed Insights are perfect for the initial diagnosis because they break down the loading timeline into audits. For example, a Lighthouse report will tell you exactly which resources are render-blocking, how large each image is, and how much main-thread time each script consumes.
Measuring Locally with Lighthouse
Lighthouse is available directly in Chrome DevTools and as a CLI tool, which makes it the fastest way to get a lab baseline. To measure a page locally, open the page, press F12 to open DevTools, and switch to the Lighthouse panel. Choose the categories you care about (Performance is the one that matters for Core Web Vitals) and run the audit. The report takes about 30 seconds and produces a score from 0 to 100 along with a detailed list of opportunities.
Keep in mind that a local Lighthouse run reflects the state of the machine and network you are testing on. Run it several times and use the median, and prefer the mobile emulation profile because that is what Google uses for ranking.
Field Data with the web-vitals Library
The most accurate way to measure field data is to collect it yourself. Google provides the web-vitals JavaScript library, which you can load with a single script tag. The library exposes the three metrics plus a handful of secondary ones, and it can send results to any analytics backend.
A minimal implementation looks like this:
<script src="https://unpkg.com/web-vitals"></script>
<script>
webVitals.onLCP((metric) => console.log('LCP', metric.value));
webVitals.onINP((metric) => console.log('INP', metric.value));
webVitals.onCLS((metric) => console.log('CLS', metric.value));
</script>
Once you are collecting real-user measurements, you can compare them against the web-vitals-calculator tool on this site to convert raw values into their percentile scores and confirm whether you are passing the Core Web Vitals assessment.
Optimizing LCP
LCP is the metric with the most moving parts, so it is the one that benefits most from a systematic approach. Start with the server, then fix resources, then optimize the largest element itself.
Improve Server Response Time
The first 20 to 50 percent of LCP is often just the time it takes for the server to send the first byte of the HTML document, also known as TTFB. A slow TTFB makes every subsequent optimization less effective, because the browser cannot start parsing or fetching resources until the HTML arrives.
The standard fixes are page caching, database query optimization, and removing slow middleware or framework bootstrapping. For PHP applications, make sure you are using opcode caching and that the configuration cache is enabled, since a framework that re-reads every config file on each request wastes hundreds of milliseconds. You should also compress the response with gzip or brotli so the HTML arrives faster over the wire.
Remove Render-Blocking Resources
The browser cannot render anything until it has fetched and parsed the render-blocking CSS and JavaScript in the <head>. Every render-blocking stylesheet is a small round trip added to your critical path. The two main strategies are inlining critical CSS and deferring the rest.
Start by identifying the CSS that is actually needed for the first render. That usually includes the layout for the header and hero section, typography, and the colors of the largest elements. Inline that CSS directly into the <head>, then load the full stylesheet with rel="preload" and onload so it applies asynchronously without blocking first paint.
For JavaScript, the default should be defer for scripts that run on load and dynamic import() for code that is only needed after interaction. Modern bundlers like Vite and webpack can split your JavaScript into many small chunks that load on demand, which reduces the amount of blocking work at startup.
Optimize the Largest Image
If your LCP element is an image, its delivery has a direct impact on the metric. The first step is to make sure the image is the right size for the viewport: serving a 3000-pixel-wide hero image to a phone that only displays 600 pixels wastes bandwidth and adds hundreds of milliseconds to LCP.
Set explicit width and height attributes on every image so the browser can reserve space, add fetchpriority="high" to the LCP image so it is fetched before other resources, and use modern formats like WebP and AVIF which are significantly smaller than JPEG and PNG. You can use the image-optimizer tool on this site to compress and resize images without losing visible quality.
Finally, make sure the image is not loaded via lazy loading. The loading="lazy" attribute is great for images below the fold, but it can delay an LCP image that is above the fold. Explicitly omit lazy loading for the hero image and consider preloading it:
<img src="hero.webp" width="1200" height="675" fetchpriority="high" alt="Hero">
Reduce Client-Side Rendering
If your LCP element is rendered by JavaScript instead of being present in the initial HTML, the browser has to download, parse, and execute JavaScript before the element can even exist. This is the slowest possible way to render LCP. The fix is to render the LCP element server-side, so it is present in the initial HTML document, and use JavaScript only to enhance it afterwards.
This is a common pattern in React, Vue, and Svelte applications. If the hero content of your page is generated client-side, consider switching to server-side rendering or static generation for those pages, or at minimum pre-render the critical above-the-fold content.
Optimizing INP
INP is dominated by main-thread work, so the optimization playbook is almost entirely about JavaScript.
Ship Less JavaScript
The most reliable way to improve INP is to ship less JavaScript in the first place. Every byte of JavaScript that loads on the page is potential main-thread work, and every third-party script is a risk of a long task that blocks interaction. Audit your bundles and remove unused libraries, replace heavy animation libraries with CSS transforms, and prefer native platform features over polyfills.
Tree-shaking and code splitting are the two bundler features that make this practical. Tree-shaking removes unused exports from your dependencies, and code splitting creates small chunks that only load when they are needed. You should also defer third-party scripts where possible: analytics, chat widgets, and social embeds rarely need to run during page load.
Break Up Long Tasks
Even with less JavaScript, a single long task can block interaction for hundreds of milliseconds. The browser treats tasks longer than 50 milliseconds as long tasks, and they are the direct cause of poor INP. The fix is to break work into smaller chunks and yield to the main thread between them.
The simplest technique is await new Promise(resolve => setTimeout(resolve, 0)) between chunks of work, which yields to the main thread. More sophisticated approaches use requestIdleCallback for non-urgent work and the native scheduler.yield() API where it is available. The goal is the same: never block the main thread for more than a few milliseconds at a time.
Avoid Layout Thrashing
Layout thrashing happens when JavaScript reads a layout property (like offsetWidth) and then writes to the DOM, forcing the browser to recalculate layout repeatedly. Reading a layout property after a DOM write is expensive because the browser must synchronously compute the layout before the read can return.
Batch your reads and writes. Read all the layout values you need first, then perform all the writes. If you are animating, prefer the Web Animations API or CSS transforms and opacity, which are compositor-friendly and do not trigger layout.
Optimizing CLS
CLS is often the easiest metric to fix because it has well-known causes with simple solutions.
Reserve Space for Images and Ads
Every image, video, and embedded ad should have explicit dimensions reserved in the layout. For images, always set width and height attributes; modern browsers respect these and reserve the space, preventing the layout from shifting when the image loads. For responsive images, the attributes work together with srcset and CSS aspect-ratio to reserve the correct space.
Ads are a special problem because their sizes are determined by the ad network at runtime. Reserve a fixed-height slot for the ad container, even if the ad fills a smaller size. If you must support multiple ad sizes, reserve the largest one and center the actual ad within the slot.
Load Fonts Without Shifting
Web fonts are a classic CLS offender because the browser initially renders text with a fallback font and then swaps to the web font when it loads, changing the size of the text. The fix is to reserve the exact metrics of the web font. Use font-display: swap combined with size-adjust, ascent-override, and descent-override so the fallback font occupies the same space as the web font.
You should also subset your fonts so that only the glyphs you actually use are downloaded. A full icon font can weigh close to a hundred kilobytes, but a subset with only the icons used on your site is dramatically smaller, which both speeds up loading and reduces the chance of a late font swap. Set explicit font-size values on text rather than relying on the natural line-height differences between fonts.
Avoid Inserting Content Above the Fold
Any DOM insertion above the fold after the initial render causes a layout shift. The most common offenders are cookie banners, notification prompts, and interstitials that slide in from the top. Render them in a reserved space or inside an overlay that does not push content around.
If you must insert content dynamically, reserve its space in advance with a placeholder element that has the expected height, or use a fixed-position overlay so the page content underneath does not move.
Building a Measurement Habit
Core Web Vitals are not a one-time fix; they drift as you add features. The best practice is to add the web-vitals library to your production site and send results to your analytics, then set up an alert for the 75th percentile values so you know the moment a new deployment hurts performance.
Every optimization in this guide compounds. Improving server response time helps LCP, shipping less JavaScript helps INP, and reserving space helps CLS. Measure before and after each change with a tool like web-vitals-calculator or PageSpeed Insights, and remember that the goal is the 75th percentile for real users, not the fastest possible result on your local machine.