Core Web Vitals: Fix LCP, CLS & INP for Better Rankings (2026)

A practical guide to optimizing the three metrics Google uses to measure real-world user experience. Covers Largest Contentful Paint, Cumulative Layout Shift, Interaction to Next Paint, and the code changes that actually move the needle.

In This Guide
  1. What Are Core Web Vitals and Why Google Cares
  2. LCP: Largest Contentful Paint
  3. CLS: Cumulative Layout Shift
  4. INP: Interaction to Next Paint
  5. Measuring with Lighthouse, PageSpeed Insights, and CrUX
  6. Framework-Specific Fixes (React, Next.js, Vanilla)
  7. Image Optimization
  8. Font Loading Strategies
  9. JavaScript Optimization
  10. Monitoring in Production
  11. Related Developer Tools
  12. Frequently Asked Questions

Core Web Vitals are three specific metrics that Google uses to evaluate real-world page experience. They measure how fast content appears, how stable the layout is while loading, and how quickly the page responds to user input. Since 2021, these metrics have been part of Google's ranking algorithm. Pages that fail them lose visibility in search results.

This guide covers each metric in detail, explains the most common causes of poor scores, and provides code-level fixes you can apply today. Every recommendation here is based on what the Chrome team documents and what consistently works in production.

Test your current scores. Before optimizing, establish a baseline. Run your site through PageSpeed Insights and note both lab and field data. Use the Meta Tag Generator to verify your pages have proper meta tags that help crawlers process your content efficiently.

What Are Core Web Vitals and Why Google Cares

Core Web Vitals are a subset of Google's broader Web Vitals initiative. As of 2026, the three Core Web Vitals are:

Metric Measures Good Needs Work Poor
LCP Loading speed ≤ 2.5s ≤ 4.0s > 4.0s
CLS Visual stability ≤ 0.1 ≤ 0.25 > 0.25
INP Responsiveness ≤ 200ms ≤ 500ms > 500ms

Google cares about these metrics because they correlate with user behavior. Research from Chrome's team shows that when a page meets all three Core Web Vitals thresholds, users are 24% less likely to abandon the page before it finishes loading. For ecommerce sites, every 100ms improvement in LCP correlates with a 1-2% increase in conversion rate.

Google collects field data through the Chrome User Experience Report (CrUX), which aggregates anonymized performance data from real Chrome users. Your CrUX data is what Google uses for ranking decisions, not your Lighthouse score. A page can score 100 in Lighthouse but still fail Core Web Vitals in the field if real users on slow devices have a different experience.

LCP: Largest Contentful Paint

Largest Contentful Paint measures how long it takes for the largest visible content element to render. This is usually a hero image, a video poster, a large heading, or a text block. The target is under 2.5 seconds.

What Counts as the LCP Element

The browser identifies the LCP element by finding the largest content element visible in the viewport at each point during page load. Candidates include:

The LCP element can change as the page loads. The browser reports the final LCP candidate before the user first interacts with the page.

Common LCP Problems and Fixes

1. Preload the LCP Resource

The most impactful single fix for slow LCP is preloading whatever resource the LCP element needs. If your hero image is referenced in CSS or loaded by JavaScript, the browser does not discover it until the CSS or JS is parsed. A preload hint in the HTML head tells the browser to start fetching immediately.

HTML
<!-- Preload your LCP image -->
<link rel="preload" as="image" href="/images/hero.webp"
      fetchpriority="high"
      type="image/webp">

<!-- For responsive images, use imagesrcset -->
<link rel="preload" as="image"
      imagesrcset="/images/hero-400.webp 400w,
                   /images/hero-800.webp 800w,
                   /images/hero-1200.webp 1200w"
      imagesizes="100vw">

2. Eliminate Render-Blocking Resources

CSS files block rendering by default. Every stylesheet in the <head> must be downloaded and parsed before the browser paints anything. Reduce this by inlining critical CSS and loading non-critical CSS asynchronously.

HTML
<!-- Inline critical CSS -->
<style>
  /* Only above-the-fold styles here */
  body { margin: 0; font-family: system-ui, sans-serif; }
  .hero { height: 100vh; display: flex; align-items: center; }
  .hero img { width: 100%; height: auto; }
</style>

<!-- Load full CSS asynchronously -->
<link rel="preload" href="/css/main.css" as="style"
      onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/css/main.css"></noscript>

Use the CSS Minifier to reduce your stylesheet size before deploying. Smaller CSS files parse faster, directly reducing render-blocking time.

3. Improve Server Response Time (TTFB)

If the initial HTML takes too long to arrive, everything else is delayed. Target a Time to First Byte (TTFB) under 800ms.

Nginx
# Enable Brotli compression
brotli on;
brotli_types text/html text/css application/javascript
             application/json image/svg+xml;
brotli_comp_level 6;

# Cache static assets for one year
location /assets/ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}

4. Set fetchpriority on the LCP Image

The fetchpriority attribute tells the browser to prioritize this image over other resources discovered at the same time.

HTML
<img src="/images/hero.webp"
     alt="Product dashboard showing analytics"
     width="1200" height="630"
     fetchpriority="high"
     decoding="async">
LCP Quick Wins

Preload + fetchpriority="high" on the LCP image is often enough to drop LCP by 500ms-1.5s. Apply these two changes first and measure before pursuing more complex optimizations.

CLS: Cumulative Layout Shift

Cumulative Layout Shift measures how much visible content moves unexpectedly during the page lifecycle. A shift happens when a visible element changes its position from one rendered frame to the next without being triggered by user input. The target is a CLS score under 0.1.

What Causes Layout Shifts

Fix 1: Always Set Image and Video Dimensions

This is the single most common cause of CLS. Every <img> and <video> element should have explicit width and height attributes. The browser uses these to calculate the aspect ratio and reserve space before the resource loads.

HTML
<!-- Good: browser reserves space immediately -->
<img src="/images/product.webp"
     alt="Product screenshot"
     width="800" height="450"
     loading="lazy">

<!-- Also good: CSS aspect-ratio -->
<style>
  .responsive-img {
    width: 100%;
    height: auto;
    aspect-ratio: 16 / 9;
  }
</style>
<img class="responsive-img" src="/images/product.webp"
     alt="Product screenshot">

Fix 2: Reserve Space for Dynamic Content

CSS
/* Reserve space for an ad slot */
.ad-container {
  min-height: 250px;
  background: #f0f0f0;
}

/* Reserve space for a cookie banner at the bottom */
.cookie-banner-placeholder {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  min-height: 80px;
}

/* Use CSS contain to isolate layout shifts */
.dynamic-content {
  contain: layout style;
}

Fix 3: Prevent Font Swap Layout Shifts

When a web font loads and replaces the fallback font, text reflows if the two fonts have different metrics. Use font-display: optional for the most stable experience (no swap at all if the font does not load in time), or use size-adjust to match the fallback font metrics to the web font.

CSS
/* Option 1: No swap at all (most stable CLS) */
@font-face {
  font-family: 'CustomFont';
  src: url('/fonts/custom.woff2') format('woff2');
  font-display: optional;
}

/* Option 2: Swap with matched fallback metrics */
@font-face {
  font-family: 'CustomFont';
  src: url('/fonts/custom.woff2') format('woff2');
  font-display: swap;
}

@font-face {
  font-family: 'CustomFont-Fallback';
  src: local('Arial');
  size-adjust: 105%;
  ascent-override: 95%;
  descent-override: 22%;
  line-gap-override: 0%;
}

body {
  font-family: 'CustomFont', 'CustomFont-Fallback', Arial, sans-serif;
}

The Webfont Generator can help you convert fonts to optimized WOFF2 format and generate the correct @font-face declarations for your project.

Fix 4: Use CSS transform for Animations

CSS
/* Bad: animating top/left triggers layout shifts */
.slide-in {
  animation: slideIn-bad 0.3s ease;
}
@keyframes slideIn-bad {
  from { top: -100px; }
  to { top: 0; }
}

/* Good: transform doesn't trigger layout */
.slide-in {
  animation: slideIn-good 0.3s ease;
}
@keyframes slideIn-good {
  from { transform: translateY(-100px); }
  to { transform: translateY(0); }
}
CLS is Session-Based

CLS measures shifts across the entire page session, not just initial load. A carousel that shifts layout 5 seconds after load still counts. Infinite scroll implementations that inject content above the viewport also cause CLS. Use the Performance panel in Chrome DevTools to identify when shifts happen.

INP: Interaction to Next Paint

Interaction to Next Paint replaced First Input Delay (FID) as a Core Web Vital in March 2024. While FID only measured the delay of the first interaction, INP measures the latency of every interaction throughout the page lifecycle and reports the worst one (the 98th percentile). The target is under 200 milliseconds.

An interaction includes the input delay (time before the handler runs), the processing time (the handler itself), and the presentation delay (time for the browser to paint the result). All three contribute to INP.

Common INP Problems

Fix 1: Break Up Long Tasks

JavaScript
// Bad: one long task blocks the main thread
function processAllItems(items) {
  items.forEach(item => {
    heavyComputation(item);  // 5ms per item x 200 items = 1000ms
  });
}

// Good: yield to the main thread between chunks
async function processAllItems(items) {
  const CHUNK_SIZE = 10;
  for (let i = 0; i < items.length; i += CHUNK_SIZE) {
    const chunk = items.slice(i, i + CHUNK_SIZE);
    chunk.forEach(item => heavyComputation(item));

    // Yield to allow the browser to process pending interactions
    await scheduler.yield();
  }
}

// Fallback if scheduler.yield() is not supported
function yieldToMain() {
  if ('scheduler' in window && 'yield' in scheduler) {
    return scheduler.yield();
  }
  return new Promise(resolve => setTimeout(resolve, 0));
}

Fix 2: Defer Non-Critical Work

JavaScript
// Bad: analytics runs during interaction
button.addEventListener('click', () => {
  updateUI();           // 10ms - user needs this
  trackAnalytics();     // 50ms - user doesn't need this
  syncToServer();       // 80ms - user doesn't need this
});

// Good: only do what the user needs, defer the rest
button.addEventListener('click', () => {
  updateUI();

  requestIdleCallback(() => {
    trackAnalytics();
    syncToServer();
  });
});

Fix 3: Avoid Forced Synchronous Layouts

JavaScript
// Bad: read-write-read pattern forces layout thrashing
elements.forEach(el => {
  const width = el.offsetWidth;     // read (forces layout)
  el.style.width = width + 10 + 'px'; // write (invalidates layout)
});
// Each iteration forces a recalculation

// Good: batch reads, then batch writes
const widths = elements.map(el => el.offsetWidth); // all reads
elements.forEach((el, i) => {
  el.style.width = widths[i] + 10 + 'px';          // all writes
});

Use the JavaScript Minifier to reduce the overall JavaScript payload your users download. Smaller files parse faster, leaving more main thread time for handling interactions.

Measuring with Lighthouse, PageSpeed Insights, and CrUX

Lighthouse (Lab Data)

Lighthouse runs in a simulated environment with throttled CPU and network. It is reproducible and great for debugging, but does not reflect real user experience. Run it from Chrome DevTools (Performance tab) or the command line.

Terminal
# Run Lighthouse from the command line
npx lighthouse https://example.com \
  --output=html --output-path=./report.html \
  --only-categories=performance

# Run with mobile throttling (default)
npx lighthouse https://example.com --preset=desktop

PageSpeed Insights (Lab + Field Data)

PageSpeed Insights at pagespeed.web.dev shows both a Lighthouse lab score and CrUX field data. The field data section at the top is what matters for SEO. If it says "Does not pass the Core Web Vitals assessment," that is what Google uses for ranking.

CrUX API (Programmatic Access)

JavaScript
// Query CrUX API for field data
async function getCrUXData(url) {
  const API_KEY = 'YOUR_API_KEY';
  const response = await fetch(
    `https://chromeuxreport.googleapis.com/v1/records:queryRecord?key=${API_KEY}`,
    {
      method: 'POST',
      body: JSON.stringify({
        url: url,
        metrics: [
          'largest_contentful_paint',
          'cumulative_layout_shift',
          'interaction_to_next_paint'
        ]
      })
    }
  );
  return response.json();
}

web-vitals Library (Real User Monitoring)

JavaScript
import { onLCP, onCLS, onINP } from 'web-vitals';

function sendToAnalytics(metric) {
  const body = JSON.stringify({
    name: metric.name,
    value: metric.value,
    rating: metric.rating,  // 'good', 'needs-improvement', 'poor'
    delta: metric.delta,
    id: metric.id,
    navigationType: metric.navigationType
  });

  // Use sendBeacon so it works even on page unload
  navigator.sendBeacon('/api/vitals', body);
}

onLCP(sendToAnalytics);
onCLS(sendToAnalytics);
onINP(sendToAnalytics);

Framework-Specific Fixes

React

React's virtual DOM can hurt INP if components re-render unnecessarily during interactions. Use React Profiler to identify slow renders.

React
import { memo, useMemo, useCallback, useTransition } from 'react';

// Use useTransition for non-urgent updates
function SearchResults({ query }) {
  const [isPending, startTransition] = useTransition();
  const [results, setResults] = useState([]);

  function handleSearch(input) {
    // Mark the results update as non-urgent
    startTransition(() => {
      const filtered = heavyFilterOperation(input);
      setResults(filtered);
    });
  }

  return (
    <div>
      <input onChange={e => handleSearch(e.target.value)} />
      {isPending ? <Spinner /> : <ResultList items={results} />}
    </div>
  );
}

// Memoize expensive child components
const ResultList = memo(function ResultList({ items }) {
  return items.map(item => <ResultCard key={item.id} {...item} />);
});

Next.js

Next.js provides built-in optimizations. Use them.

Next.js
import Image from 'next/image';
import { Inter } from 'next/font/google';

// Automatic font optimization (eliminates font CLS)
const inter = Inter({ subsets: ['latin'], display: 'swap' });

// Automatic image optimization (LCP + CLS)
export default function Hero() {
  return (
    <div className={inter.className}>
      <Image
        src="/hero.jpg"
        alt="Dashboard preview"
        width={1200}
        height={630}
        priority          {/* Preloads LCP image */}
        sizes="100vw"
        placeholder="blur"  {/* Prevents CLS with blur-up */}
      />
    </div>
  );
}

Vanilla HTML/CSS/JS

HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">

  <!-- Preload LCP image -->
  <link rel="preload" as="image" href="../img/hero.webp"
        fetchpriority="high">

  <!-- Preload critical font -->
  <link rel="preload" as="font" type="font/woff2"
        href="../fonts/main.woff2" crossorigin>

  <!-- Inline critical CSS -->
  <style>
    body { margin: 0; font-family: 'MainFont', system-ui; }
    .hero { position: relative; }
    .hero img { width: 100%; height: auto; aspect-ratio: 16/9; }
  </style>

  <!-- Async load full stylesheet -->
  <link rel="preload" href="/css/main.css" as="style"
        onload="this.onload=null;this.rel='stylesheet'">
</head>
<body>
  <div class="hero">
    <img src="/img/hero.webp" alt="Hero"
         width="1200" height="675"
         fetchpriority="high" decoding="async">
  </div>

  <!-- Defer all JavaScript -->
  <script defer src="../js/app.js"></script>
</body>
</html>

Image Optimization

Images are typically the largest resources on a page and the most common LCP element. Optimizing them has the highest impact per effort.

Use Modern Formats

WebP delivers 25-35% smaller files than JPEG at equivalent quality. AVIF delivers 40-50% smaller files but has less browser support. Use the <picture> element to serve the best format each browser supports.

HTML
<picture>
  <source srcset="/img/hero.avif" type="image/avif">
  <source srcset="/img/hero.webp" type="image/webp">
  <img src="/img/hero.jpg" alt="Product dashboard"
       width="1200" height="630"
       loading="lazy" decoding="async">
</picture>

Responsive Images with srcset

HTML
<img srcset="/img/hero-400.webp 400w,
             /img/hero-800.webp 800w,
             /img/hero-1200.webp 1200w,
             /img/hero-1600.webp 1600w"
     sizes="(max-width: 600px) 100vw,
            (max-width: 1200px) 80vw,
            1200px"
     src="/img/hero-800.webp"
     alt="Product dashboard"
     width="1200" height="630"
     loading="lazy" decoding="async">

Lazy Load Below-the-Fold Images

Use native lazy loading for all images below the fold. Never lazy-load the LCP image.

HTML
<!-- LCP image: load eagerly with high priority -->
<img src="/img/hero.webp" alt="Hero"
     width="1200" height="630"
     fetchpriority="high" decoding="async">

<!-- Below-the-fold images: lazy load -->
<img src="/img/feature-1.webp" alt="Feature 1"
     width="600" height="400"
     loading="lazy" decoding="async">

<img src="/img/feature-2.webp" alt="Feature 2"
     width="600" height="400"
     loading="lazy" decoding="async">

The Image Compressor can reduce image file sizes directly in your browser before uploading. It processes images client-side without sending your files to any server.

Font Loading Strategies

Fonts affect both LCP (if text is the LCP element, it is invisible until the font loads) and CLS (when the fallback font is replaced by the web font). The right loading strategy depends on your priorities.

Strategy 1: Preload + font-display: swap

Best for sites where brand typography matters. The text appears immediately in the fallback font, then swaps to the web font when it loads. The swap may cause a small CLS.

HTML
<link rel="preload" as="font" type="font/woff2"
      href="../fonts/inter-var.woff2" crossorigin>

<style>
  @font-face {
    font-family: 'Inter';
    src: url('/fonts/inter-var.woff2') format('woff2');
    font-display: swap;
    font-weight: 100 900;
  }
</style>

Strategy 2: font-display: optional (Zero CLS)

Best for CLS-sensitive pages. The browser uses the web font only if it loads within the first 100ms. Otherwise, it sticks with the fallback for the entire page session. No swap means zero CLS from fonts.

Strategy 3: Subset Your Fonts

Most Latin-character sites only need a subset of the full Unicode range. Subsetting can reduce a 100KB font file to under 20KB.

Terminal
# Subset a font to Latin characters using pyftsubset
pip install fonttools brotli
pyftsubset Inter-Regular.ttf \
  --output-file=Inter-Regular-Latin.woff2 \
  --flavor=woff2 \
  --layout-features='kern,liga' \
  --unicodes='U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD'

Strategy 4: System Font Stack (Maximum Performance)

If brand typography is not critical, a system font stack eliminates font loading entirely. Zero download, zero CLS, zero LCP impact from fonts.

CSS
body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI',
               Roboto, Oxygen, Ubuntu, Cantarell, 'Helvetica Neue',
               sans-serif;
}

JavaScript Optimization

JavaScript is the primary cause of poor INP scores and a major contributor to slow LCP when it blocks rendering. The goal is to send less JavaScript and run it more efficiently.

Code Splitting

Ship only the JavaScript the current page needs. Most bundlers support dynamic imports that split code into separate chunks loaded on demand.

JavaScript
// Instead of importing everything upfront
import { heavyChartLibrary } from './charts';

// Load it only when needed
const chartButton = document.querySelector('#show-chart');
chartButton.addEventListener('click', async () => {
  const { heavyChartLibrary } = await import('./charts');
  heavyChartLibrary.render('#chart-container', data);
});

Defer and Async

HTML
<!-- BAD: blocks HTML parsing -->
<script src="../js/app.js"></script>

<!-- GOOD: defer downloads in parallel, executes after parse -->
<script defer src="../js/app.js"></script>

<!-- GOOD: async downloads in parallel, executes when ready -->
<!-- Use for scripts that don't depend on DOM or other scripts -->
<script async src="../js/analytics.js"></script>

Tree Shaking

Tree shaking removes unused code during the build step. It works automatically with ES modules in modern bundlers (Vite, webpack 5+, Rollup, esbuild). Make sure you are using named imports.

JavaScript
// Bad: imports the entire library (lodash is 70KB+)
import _ from 'lodash';
const result = _.debounce(fn, 300);

// Good: imports only the function you use (~1KB)
import debounce from 'lodash/debounce';
const result = debounce(fn, 300);

// Best: write a tiny debounce yourself (~200 bytes)
function debounce(fn, ms) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), ms);
  };
}

Third-Party Script Management

Third-party scripts (analytics, ads, chat widgets) are the most common cause of poor INP on otherwise fast sites. Load them after your critical content.

JavaScript
// Load third-party scripts only after the page is interactive
function loadThirdParty() {
  // Google Analytics
  const ga = document.createElement('script');
  ga.src = 'https://www.googletagmanager.com/gtag/js?id=G-XXXXXX';
  ga.async = true;
  document.head.appendChild(ga);

  // Chat widget
  const chat = document.createElement('script');
  chat.src = 'https://chat-widget.example.com/loader.js';
  chat.async = true;
  document.head.appendChild(chat);
}

// Wait for the page to be fully loaded and idle
if ('requestIdleCallback' in window) {
  requestIdleCallback(loadThirdParty);
} else {
  window.addEventListener('load', () => {
    setTimeout(loadThirdParty, 2000);
  });
}

After optimizing, use the HTML Minifier to strip whitespace and comments from your production HTML, and the JavaScript Minifier to compress your JS bundles.

Monitoring in Production

Lab testing tells you what is possible. Production monitoring tells you what is actually happening. You need both.

Real User Monitoring (RUM) Setup

The web-vitals library from Google is the standard for collecting Core Web Vitals in production. Send the data to your analytics backend and dashboard it.

JavaScript
import { onLCP, onCLS, onINP } from 'web-vitals/attribution';

function sendMetric(metric) {
  // Attribution tells you WHY the score is what it is
  const attribution = metric.attribution;

  const payload = {
    name: metric.name,
    value: Math.round(metric.value),
    rating: metric.rating,
    url: location.href,
    // LCP: what element was it?
    ...(metric.name === 'LCP' && {
      lcpElement: attribution.element,
      lcpUrl: attribution.url,
      lcpTTFB: attribution.timeToFirstByte,
      lcpLoadDelay: attribution.resourceLoadDelay
    }),
    // CLS: what shifted?
    ...(metric.name === 'CLS' && {
      clsElement: attribution.largestShiftTarget,
      clsTime: attribution.largestShiftTime
    }),
    // INP: what interaction was slow?
    ...(metric.name === 'INP' && {
      inpElement: attribution.interactionTarget,
      inpType: attribution.interactionType,
      inpDelay: attribution.inputDelay,
      inpProcessing: attribution.processingDuration,
      inpPresentation: attribution.presentationDelay
    })
  };

  navigator.sendBeacon('/api/vitals', JSON.stringify(payload));
}

onLCP(sendMetric);
onCLS(sendMetric);
onINP(sendMetric);

Alerting Thresholds

Set alerts for when your 75th percentile (p75) crosses the "good" threshold. CrUX uses p75 for its assessments, so your p75 is what matters for SEO.

CrUX Dashboard

Google provides a free CrUX Dashboard built on Looker Studio. Connect it to your origin and get a historical view of your Core Web Vitals over time. CrUX data updates every 28 days (rolling average), so plan for a 4-week feedback cycle after making changes.

Test your pages across different device sizes with the Responsive Tester to verify your optimizations work on mobile, tablet, and desktop viewports.

Field data lags lab data. After deploying a fix, your Lighthouse score improves immediately but CrUX field data takes 28 days to fully reflect the change. Do not revert a fix because CrUX has not updated yet. Validate in Lighthouse first, then wait for field data to confirm.

Related Developer Tools

Free browser-based tools to help you optimize Core Web Vitals. All run client-side with no data sent to any server.


Frequently Asked Questions

Core Web Vitals are three metrics Google uses to measure real-world user experience on web pages: Largest Contentful Paint (LCP) measures loading speed, Cumulative Layout Shift (CLS) measures visual stability, and Interaction to Next Paint (INP) measures responsiveness. They directly affect SEO rankings as part of Google's page experience signals. Pages that pass all three thresholds (LCP under 2.5 seconds, CLS under 0.1, INP under 200 milliseconds) receive a ranking boost in search results. Google uses field data from the Chrome User Experience Report (CrUX) to evaluate these metrics, meaning lab scores alone are not enough.

Interaction to Next Paint (INP) officially replaced First Input Delay (FID) as a Core Web Vital in March 2024. While FID only measured the delay of the first interaction on a page, INP measures the latency of all interactions throughout the entire page lifecycle and reports the worst one (technically the 98th percentile). This makes INP a much more comprehensive responsiveness metric. A good INP score is under 200 milliseconds. To improve INP, break up long tasks on the main thread, use requestIdleCallback or scheduler.yield() for non-critical work, minimize JavaScript execution time, and avoid forced synchronous layouts.

The most common LCP fixes target four areas. First, optimize the LCP resource itself: if it is an image, compress it, serve it in WebP or AVIF format, and add explicit width and height attributes. Second, eliminate render-blocking resources by deferring non-critical CSS and JavaScript, inlining critical CSS, and using font-display: swap for web fonts. Third, improve server response time by using a CDN, enabling compression (Brotli or gzip), and setting proper cache headers. Fourth, preload the LCP resource with a link rel=preload tag so the browser discovers it immediately instead of waiting for CSS or JavaScript to reference it. The target is LCP under 2.5 seconds on a mid-tier mobile device.

Cumulative Layout Shift (CLS) is caused by elements that move after the page has started rendering. The most common causes are images and videos without dimensions (the browser does not know how much space to reserve until they load), dynamically injected content like ads or banners that push existing content down, web fonts that cause text to reflow when they load (FOUT), and CSS animations that trigger layout changes. To prevent CLS: always set width and height attributes on images and videos or use CSS aspect-ratio; reserve space for ad slots and dynamic content with min-height; use font-display: optional or font-display: swap with size-adjust to minimize font swap shifts; prefer CSS transform animations over properties that trigger layout like top, left, width, or height. A good CLS score is under 0.1.

Use both, but understand what each measures. Lighthouse runs in a simulated environment (lab data) and gives you a controlled, reproducible score that is useful for debugging specific issues during development. PageSpeed Insights shows both lab data and field data from the Chrome User Experience Report (CrUX), which is what Google actually uses for ranking decisions. Field data reflects real user experiences across different devices and network conditions. For SEO purposes, field data matters most. For debugging, lab data with Lighthouse is more actionable. The ideal workflow is: check PageSpeed Insights to see your real-world scores, then use Lighthouse DevTools with performance tracing to identify and fix specific bottlenecks, then verify the fix in the field over the following 28 days as CrUX data updates.

NT

Christian Bucher

We build free developer tools including image compressors, CSS minifiers, responsive testers, and 269 more. All browser-based, no signup required.

269 Developer Tools, One Place

Browse 269 indexed tool pages with no QTool account required, and inspect the source on GitHub.

Open Source — Free Forever Try Free Tools

Related Articles

Built by Miguel

Need a custom tool or website?

From . Delivered in 24-48h. You own the code.

View Services →