Core Web Vitals Optimization: A Practical Guide
A hands-on guide to optimizing Core Web Vitals — LCP, INP, and CLS — with specific fixes, code examples, and measurement strategies that improve both rankings and user experience.
Core Web Vitals are Google's quantified answer to the question "does this page provide a good user experience?" They measure three specific aspects of real-world page experience: loading speed (LCP), interactivity (INP), and visual stability (CLS). Since 2021, they have been a confirmed ranking factor, and their importance has only increased as Google refines its page experience signals.
The business case is straightforward. Vodafone improved LCP by 31% and saw an 8% increase in sales. NDTV reduced LCP by 55% and saw a 50% decrease in bounce rate. Yelp improved CLS and saw a 15% increase in conversion rate. Performance is not just about SEO rankings — it directly impacts revenue.
This guide covers each metric in detail, explains what causes poor scores, and provides specific, implementable fixes.
Understanding the Three Metrics
Largest Contentful Paint (LCP)
What it measures: How long it takes for the largest visible content element to fully render. This is typically a hero image, a video poster, or a large text block.
Threshold:
- Good: under 2.5 seconds
- Needs improvement: 2.5 - 4.0 seconds
- Poor: over 4.0 seconds
Why it matters: LCP is the user's perception of when the page has "loaded." Until the largest content element renders, the page feels incomplete.
Interaction to Next Paint (INP)
What it measures: The responsiveness of the page to user interactions. INP observes all clicks, taps, and keyboard inputs during a page visit, and reports the worst interaction latency (with outlier adjustment).
Threshold:
- Good: under 200 milliseconds
- Needs improvement: 200 - 500 milliseconds
- Poor: over 500 milliseconds
Why it matters: INP replaced First Input Delay (FID) in March 2024 because FID only measured the first interaction. INP measures all interactions throughout the page lifecycle, capturing jank that occurs after initial load.
Cumulative Layout Shift (CLS)
What it measures: The total of unexpected layout shifts that occur during the page's lifespan. A layout shift happens when a visible element changes position from one rendered frame to the next without user interaction.
Threshold:
- Good: under 0.1
- Needs improvement: 0.1 - 0.25
- Poor: over 0.25
Why it matters: Layout shifts cause misclicks, reading interruption, and user frustration. The "I tried to tap a button but the page shifted and I hit an ad" experience is exactly what CLS penalizes.
Measuring Core Web Vitals
Before optimizing, you need accurate measurement from both lab and field data sources.
Field Data (Real User Metrics)
Field data comes from actual users visiting your site. This is what Google uses for ranking decisions.
- Google Search Console (Core Web Vitals report): Shows pass/fail status by URL group, based on Chrome User Experience Report (CrUX) data
- CrUX Dashboard (via BigQuery or CrUX API): Raw field data at origin and URL level, updated monthly
- PageSpeed Insights (field data section): Shows the 75th percentile of real user experiences
Lab Data (Synthetic Testing)
Lab data is collected in controlled environments. It is useful for debugging but does not represent real user experience.
- PageSpeed Insights (lab data section): Runs a Lighthouse audit on demand
- Chrome DevTools (Performance panel): Detailed frame-by-frame analysis
- WebPageTest: Multi-location, multi-device testing with filmstrip views
- Lighthouse CI: Automated Lighthouse audits in your CI/CD pipeline
Monitoring Setup
For ongoing monitoring, implement the web-vitals JavaScript library:
import { onLCP, onINP, onCLS } from 'web-vitals';
function sendToAnalytics(metric) {
const body = JSON.stringify({
name: metric.name,
value: metric.value,
rating: metric.rating,
delta: metric.delta,
id: metric.id,
navigationType: metric.navigationType,
});
// Use sendBeacon for reliability during page unload
navigator.sendBeacon('/api/analytics/vitals', body);
}
onLCP(sendToAnalytics);
onINP(sendToAnalytics);
onCLS(sendToAnalytics);
This gives you real user data segmented by page, device, connection speed, and geography — far more actionable than monthly CrUX reports.
Optimizing Largest Contentful Paint (LCP)
LCP failures have four root causes, in order of frequency:
1. Slow Server Response Time (TTFB)
Time to First Byte (TTFB) is the foundation of LCP. If your server takes 2 seconds to respond, your LCP cannot be under 2.5 seconds regardless of frontend optimization.
Fixes:
- Use a CDN: Serve content from edge servers geographically close to users. Cloudflare, Fastly, AWS CloudFront, and Vercel's Edge Network all reduce TTFB to under 100ms for cached content.
- Implement server-side caching: Cache rendered HTML for pages that do not change on every request. Incremental Static Regeneration (ISR) in Next.js, stale-while-revalidate cache headers, or a Varnish/Nginx cache layer.
- Optimize database queries: Slow queries are a common TTFB bottleneck. Add indexes, reduce N+1 queries, implement query result caching (Redis/Memcached).
- Upgrade your hosting: Shared hosting plans often have inconsistent TTFB. Move to managed cloud hosting (Vercel, Netlify, AWS, GCP) with predictable performance.
Target TTFB: Under 800ms, ideally under 200ms.
2. Render-Blocking Resources
CSS and JavaScript files in the <head> block rendering until they are downloaded and parsed.
Fixes:
- Inline critical CSS: Extract the CSS needed to render above-the-fold content and inline it in the
<head>. Load the full stylesheet asynchronously.<style>/* Critical CSS inlined here */</style> <link rel="preload" href="/styles/main.css" as="style" onload="this.onload=null;this.rel='stylesheet'"> - Defer non-critical JavaScript: Add
deferorasyncto script tags that are not needed for initial render.<script src="/analytics.js" defer></script> - Remove unused CSS: Tools like PurgeCSS or the Coverage tab in Chrome DevTools identify CSS rules that are never applied on a given page. E-commerce sites commonly load 200-400KB of CSS but use only 30-50KB per page.
3. Slow Resource Load Times
The LCP element itself (image, video, font) takes too long to download.
Fixes:
- Optimize images: Convert to WebP or AVIF format (30-50% smaller than JPEG). Use responsive
srcsetattributes to serve appropriately sized images.<img src="hero-800.webp" srcset="hero-400.webp 400w, hero-800.webp 800w, hero-1200.webp 1200w" sizes="(max-width: 768px) 100vw, 800px" alt="Descriptive alt text" width="800" height="450" fetchpriority="high"> - Preload the LCP image: Tell the browser to start downloading the LCP image immediately.
<link rel="preload" as="image" href="hero-800.webp" imagesrcset="hero-400.webp 400w, hero-800.webp 800w, hero-1200.webp 1200w" imagesizes="(max-width: 768px) 100vw, 800px"> - Use
fetchpriority="high"on the LCP image element to prioritize its download over other images. - Optimize web fonts: Use
font-display: swapto prevent font-loading from blocking text rendering. Preload critical font files. Self-host fonts to avoid third-party connection overhead.
4. Client-Side Rendering Delays
Single-page applications (SPAs) that render content via JavaScript after the initial HTML loads suffer inherently poor LCP.
Fixes:
- Use Server-Side Rendering (SSR) or Static Site Generation (SSG): Frameworks like Next.js, Nuxt, Remix, and Astro render HTML on the server, delivering a complete page that the browser can display immediately.
- Stream HTML with React Server Components: Next.js App Router with React Server Components sends HTML progressively, allowing the browser to render content as it arrives.
- Avoid layout-dependent JavaScript: If JavaScript must run before the LCP element becomes visible (data fetching, template rendering), the LCP will always be delayed.
Optimizing Interaction to Next Paint (INP)
INP measures how quickly your page responds to user input. Poor INP means the main thread is blocked by JavaScript execution.
Identifying INP Bottlenecks
Use Chrome DevTools Performance panel:
- Record a performance trace while interacting with the page
- Look for "Long Tasks" (yellow/red bars) on the main thread that exceed 50ms
- Click on the long task to identify the JavaScript function or event handler causing the delay
The web-vitals library's onINP callback includes an entries property that tells you which specific interaction had the worst latency and what event type triggered it.
Fix 1: Break Up Long Tasks
Any JavaScript task that runs for more than 50ms blocks the main thread and delays input processing.
Strategy: Yield to the main thread periodically using scheduler.yield() (Chrome 129+) or the setTimeout(0) pattern:
async function processLargeDataset(items) {
for (let i = 0; i < items.length; i++) {
processItem(items[i]);
// Yield every 5ms to keep the thread responsive
if (i % 100 === 0) {
await scheduler.yield();
}
}
}
Fix 2: Reduce JavaScript Bundle Size
Less JavaScript means fewer long tasks. Audit your bundles:
- Code splitting: Load JavaScript only when it is needed. Route-based code splitting is the minimum (Next.js and other frameworks do this automatically). Component-level code splitting with
React.lazy()or dynamic imports further reduces initial bundle size. - Remove unused code: Run Webpack Bundle Analyzer or
source-map-explorerto identify large, unused dependencies. Replace heavy libraries with lighter alternatives (e.g.,date-fnsinstead ofmoment.js,preactinstead ofreactfor widget-like components). - Tree shaking: Ensure your bundler eliminates unused exports. Named imports (
import { debounce } from 'lodash-es') tree-shake better than default imports (import _ from 'lodash').
Fix 3: Optimize Event Handlers
Expensive event handlers are the direct cause of poor INP scores.
- Debounce scroll and resize handlers: Do not run computation on every scroll/resize event — use
requestAnimationFrameor a debounce function. - Avoid forced synchronous layouts: Reading layout properties (
offsetHeight,getBoundingClientRect) after writing style properties forces the browser to recalculate layout synchronously. Batch reads and writes separately. - Move computation off the main thread: Use Web Workers for heavy data processing (sorting, filtering, calculations) that does not need DOM access.
Fix 4: Optimize Third-Party Scripts
Analytics, chat widgets, A/B testing tools, and ad scripts are among the most common INP offenders.
- Load third-party scripts with
asyncordefer - Use
requestIdleCallbackto initialize non-critical third-party services - Consider loading chat widgets only after user interaction (click a "Chat" button)
- Audit third-party script impact using Chrome DevTools Performance panel — filter by "third-party" in the call tree
Optimizing Cumulative Layout Shift (CLS)
CLS issues are almost always caused by missing dimension attributes, late-loading content, or dynamic content injection.
Fix 1: Set Explicit Dimensions on Media
The #1 CLS fix. Specify width and height attributes on all images and videos:
<!-- This causes CLS -->
<img src="photo.webp" alt="...">
<!-- This prevents CLS -->
<img src="photo.webp" alt="..." width="800" height="450">
The browser uses these attributes to calculate an aspect ratio and reserve the correct space before the image downloads. CSS aspect-ratio works similarly:
.video-container {
aspect-ratio: 16 / 9;
width: 100%;
}
Fix 2: Reserve Space for Dynamic Content
Ads, embeds, iframes, and lazy-loaded content that inject into the page without reserved space cause layout shifts.
- Ads: Use CSS to set minimum dimensions on ad containers matching the expected ad size
- Embeds (YouTube, Twitter, maps): Wrap in a container with a fixed aspect ratio
- Cookie banners: Position them with
position: fixedor at the top of the page, so they do not push content down - Dynamic notifications: Use overlays or fixed positioning, not content insertion that shifts the page
Fix 3: Avoid Late-Loading Web Fonts
Web fonts that swap in after system fonts cause a text layout shift (FOUT — Flash of Unstyled Text).
Prevention:
- Preload critical font files:
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin> - Use
font-display: optionalfor non-critical fonts (prevents layout shift entirely, at the cost of potentially not showing the custom font on slow connections) - Use
size-adjustin your@font-faceto match the fallback font's metrics to the web font, minimizing the visual shift when the font swaps
Fix 4: Use CSS contain and Content-Visibility
For sections below the fold, content-visibility: auto tells the browser to skip rendering until the element is near the viewport. Combined with contain-intrinsic-size, this prevents layout shifts from deferred rendering:
.below-fold-section {
content-visibility: auto;
contain-intrinsic-size: 0 500px; /* Estimated height */
}
Framework-Specific Optimizations
Next.js
Next.js provides several built-in optimizations:
next/image: Automatically optimizes images, serves WebP/AVIF, generatessrcset, lazy loads by default, and prevents CLS with automatic width/heightnext/font: Self-hosts fonts, eliminates external font requests, appliessize-adjustautomatically- App Router with Server Components: Reduces client-side JavaScript by rendering on the server
- Partial Prerendering (experimental): Combines static and dynamic rendering for optimal TTFB
WordPress
WordPress sites face unique CWV challenges due to plugin bloat:
- Use a caching plugin (WP Rocket, W3 Total Cache) for TTFB optimization
- Use ShortPixel or Imagify for automatic image optimization and WebP conversion
- Audit plugins with Query Monitor — disable plugins that add excessive JavaScript
- Implement lazy loading via the native
loading="lazy"attribute (WordPress 5.5+ does this automatically) - Consider a page builder like Bricks or GeneratePress that produces clean, lightweight HTML
Monitoring and Reporting
Setting Up CWV Monitoring in CI/CD
Prevent performance regressions by running Lighthouse CI on every pull request:
# GitHub Actions example
- name: Run Lighthouse CI
uses: treosh/lighthouse-ci-action@v11
with:
urls: |
https://staging.example.com/
https://staging.example.com/products/sample
budgetPath: ./lighthouse-budget.json
uploadArtifacts: true
Define performance budgets that fail the build if metrics regress:
[{
"path": "/*",
"timings": [
{ "metric": "largest-contentful-paint", "budget": 2500 },
{ "metric": "interactive", "budget": 3500 },
{ "metric": "cumulative-layout-shift", "budget": 0.1 }
]
}]
Real User Monitoring (RUM) Dashboard
Build a dashboard that tracks CWV percentiles over time, segmented by:
- Page type (homepage, product page, blog post, category page)
- Device type (mobile vs. desktop)
- Connection speed (4G, 3G, Wi-Fi)
- Geography (CDN effectiveness varies by region)
- Browser (Chrome, Safari, Firefox — each renders differently)
This segmentation reveals that your "average" may mask a great mobile experience and a terrible desktop experience (or vice versa).
Prioritization Framework
When everything needs fixing, start with the highest-impact, lowest-effort optimizations:
| Priority | Fix | Effort | Impact |
|---|---|---|---|
| 1 | Add image dimensions (CLS) | Low | High |
| 2 | Enable CDN / caching (LCP) | Low | High |
| 3 | Preload LCP image (LCP) | Low | Medium-High |
| 4 | Defer non-critical JS (LCP, INP) | Low | Medium-High |
| 5 | Optimize images to WebP/AVIF (LCP) | Medium | High |
| 6 | Inline critical CSS (LCP) | Medium | Medium |
| 7 | Code splitting (INP) | Medium | Medium |
| 8 | SSR / SSG migration (LCP) | High | Very High |
| 9 | Web Worker offloading (INP) | High | Medium |
For websites that need a comprehensive performance audit and optimization plan, our SEO and web performance services include Core Web Vitals assessment, implementation, and ongoing monitoring. If your site is failing CWV checks and you are not sure where to start, request a performance audit.
FAQ
Do Core Web Vitals actually affect rankings?
Yes, but they are one of many ranking factors. Google confirmed CWV as a ranking signal in 2021 and has strengthened its weight over time. The impact is most noticeable for competitive queries where multiple pages have similar content quality and backlink profiles — CWV becomes the tiebreaker. Sites that fail CWV will not necessarily drop in rankings, but they will struggle to compete against sites with similar content that pass CWV.
Should I optimize for lab data or field data?
Optimize using lab data (Lighthouse, DevTools), but measure success using field data (CrUX, Search Console). Google uses field data for ranking decisions. Lab data is useful for diagnosing specific issues because it provides detailed performance traces, but it does not represent the diversity of real user devices, network conditions, and browsing contexts. Always validate lab improvements by monitoring field data over the subsequent 28-day CrUX collection period.
My CWV are green on desktop but red on mobile. Which matters more?
Mobile. Google uses mobile-first indexing, meaning your mobile CWV scores are what Google uses for ranking decisions — even for users searching on desktop. Additionally, mobile users are on slower devices with weaker connections, so poor mobile CWV directly impacts a larger share of your audience. Prioritize mobile optimization, then address desktop issues.
How long does it take for CWV improvements to affect rankings?
CrUX data is collected over a rolling 28-day window and updated monthly. After you deploy improvements, expect 4-8 weeks before the changes are reflected in CrUX data and Search Console. The ranking impact may take an additional 2-4 weeks to materialize as Google recrawls and reprocesses your pages. Total timeline from fix to ranking movement: 6-12 weeks.
Need Help With Your Project?
Our team of experts is ready to help you build, grow, and succeed. Get a free consultation today.
Book Free Consultation