Overview

Core Web Vitals (CWV) are the three Google signals that directly affect search ranking: Largest Contentful Paint (LCP), Interaction to Next Paint (INP), and Cumulative Layout Shift (CLS). Fix them in that order: LCP has the biggest ranking impact, INP is measured by real user interactions, and CLS is the easiest to eliminate once you know the cause. The reference page for each metric lives in core-web-vitals.

Prerequisites

  • The site deployed to a publicly reachable URL. Lighthouse can also audit localhost, but CrUX data is production-only.
  • Chrome or Chromium installed locally for Lighthouse CLI.
  • Node 20 or newer for the Lighthouse CLI.
  • Google Search Console access for CrUX data on the live domain.

Steps

1. Run Lighthouse from the CLI

Lighthouse simulates a mobile device on a slow 4G connection, which is the worst-case scenario Google cares most about.

npm install -g lighthouse
lighthouse https://example.com \
  --only-categories=performance \
  --output=json \
  --output-path=./lh-report.json \
  --preset=desktop  # omit for mobile (default)

Extract the three CWV scores:

node -e "
const r = require('./lh-report.json');
const a = r.audits;
console.log('LCP:', a['largest-contentful-paint'].displayValue);
console.log('INP:', a['interaction-to-next-paint']?.displayValue || 'n/a');
console.log('CLS:', a['cumulative-layout-shift'].displayValue);
"

2. Check CrUX data in Search Console

Lighthouse is lab data; CrUX is real user data. Open Google Search Console > Core Web Vitals report. Expand each URL group to see which pages fail in the field. Field data lags 28 days; lab data reflects the current deploy.

For pages with no CrUX data (new or low-traffic pages), rely on Lighthouse. For high-traffic pages, prioritize CrUX data over Lighthouse because it captures the distribution of real user conditions.

3. Instrument with the web-vitals JS library

The web-vitals library reports real-user CWV from the browser. Add it to the page to capture production measurements beyond what CrUX aggregates.

npm install web-vitals
import { onLCP, onINP, onCLS } from "web-vitals";
 
function sendToAnalytics(metric: { name: string; value: number }) {
  // Send to your analytics endpoint
  navigator.sendBeacon("/analytics", JSON.stringify(metric));
}
 
onLCP(sendToAnalytics);
onINP(sendToAnalytics);
onCLS(sendToAnalytics);

4. Triage LCP first

LCP targets: good under 2.5 s, needs improvement up to 4 s, poor above 4 s.

Common causes and fixes:

  • Large unoptimized hero image: convert to AVIF/WebP and add fetchpriority="high". See optimize-images-for-web.
  • Render-blocking CSS or fonts: inline critical CSS, use font-display: swap, and preconnect to font origins.
  • Slow server response (TTFB): cache the HTML response. See nextjs-caching for Next.js ISR and static generation options.
  • LCP element is off-screen on mobile: confirm the LCP element is in the viewport; Lighthouse reports which element is the LCP candidate.

5. Triage INP second

INP targets: good under 200 ms, needs improvement up to 500 ms, poor above 500 ms.

INP is the worst interaction during the user’s visit. Common causes:

  • Heavy JavaScript on the main thread: code-split and defer non-critical scripts.
  • Synchronous DOM updates in event handlers: batch reads and writes with requestAnimationFrame.
  • Third-party scripts blocking input: load third-party scripts with async or defer, or use a facade.

Measure with Chrome DevTools > Performance panel; record while clicking buttons or typing. The Long Tasks panel highlights tasks over 50 ms.

6. Triage CLS last

CLS targets: good under 0.1, needs improvement up to 0.25, poor above 0.25.

Layout shift is caused by elements that appear or change size without reserved space. Fix in this order:

  1. Add explicit width and height to all <img> and <video> elements.
  2. Reserve space for ads and embeds with min-height.
  3. Avoid inserting content above existing content after load; use transform for animations instead of properties that affect layout. See css-animations for layout-safe animation patterns.
<!-- Before: no dimensions, causes layout shift -->
<img src="hero.webp" alt="Hero">
 
<!-- After: dimensions reserved, no shift -->
<img src="hero.webp" width="1200" height="600" alt="Hero">

7. Re-measure after each fix

Deploy one fix at a time and re-run Lighthouse before moving to the next. Each change compounds; measuring after every fix confirms which ones had impact.

lighthouse https://example.com --only-categories=performance --output=json \
  --output-path=./lh-after.json

Verify it worked

# 1. Lighthouse performance score is above 90.
node -e "const r=require('./lh-after.json'); console.log(r.categories.performance.score * 100)"
# 91 or above
 
# 2. LCP is under 2.5 s.
node -e "const r=require('./lh-after.json'); console.log(r.audits['largest-contentful-paint'].numericValue)"
# under 2500
 
# 3. CLS is under 0.1.
node -e "const r=require('./lh-after.json'); console.log(r.audits['cumulative-layout-shift'].numericValue)"
# under 0.1

Common errors

  • Lighthouse score varies between runs. Network and CPU throttling introduces variance. Run 3 times and average, or use --throttling-method=provided for stable CI results.
  • CrUX shows “Not enough data”. The page has fewer than 1,000 real-user sessions in the last 28 days. Use Lighthouse as the sole benchmark for low-traffic pages.
  • CLS is zero in Lighthouse but high in CrUX. Lighthouse does not scroll; many CLS-causing elements appear only after scroll or after ads load. Add web-vitals instrumentation and measure in production.
  • INP is missing from Lighthouse output. INP requires user interaction; Lighthouse only simulates page load. Measure INP with the web-vitals library or with Chrome’s Performance panel during a recorded session.
  • Score regresses after a code change. Images added without width/height or a new third-party script is the most common cause. Run Lighthouse in CI on every PR to catch regressions before they ship.