How to Improve Core Web Vitals Score on a React Website: LCP, CLS, and INP Fixes

One of the most popular services we offer is ongoing website maintenance because most clients we work with become return clients.

How to Improve Core Web Vitals Score on a React Website: LCP, CLS, and INP Fixes

If your React app is failing Google’s Core Web Vitals, you are not alone. Most single-page applications struggle with LCP, CLS, and the newer INP metric that replaced FID in March 2024. The good news: with the right diagnostic workflow and a handful of React-specific patterns, you can usually move from red to green in a single sprint.

This is a hands-on guide. No generic advice like “optimize your images”. We’ll open Lighthouse, read the actual numbers, and ship code that fixes them.

What Are Core Web Vitals in 2026?

Google currently tracks three Core Web Vitals as ranking signals:

Metric What it measures Good threshold
LCP (Largest Contentful Paint) Time to render the biggest above-the-fold element < 2.5s
INP (Interaction to Next Paint) Responsiveness across all user interactions < 200ms
CLS (Cumulative Layout Shift) Visual stability of the page < 0.1
react performance dashboard

Step 1: Measure Before You Touch Anything

Before optimizing, install the official web-vitals library to capture real user data, not just lab data.

npm install web-vitals

In your index.js or main.tsx:

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

function sendToAnalytics(metric) {
  const body = JSON.stringify(metric);
  // Use sendBeacon so it works during page unload
  navigator.sendBeacon('/analytics', body);
  console.log(metric.name, metric.value);
}

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

Then open Chrome DevTools, go to the Lighthouse tab, select Performance + Mobile, and run an audit. Note the failing metric and the file flagged as the culprit. That is your starting point.

react performance dashboard

Step 2: Fixing LCP in React

In most React apps, the LCP element is either a hero image or a large heading that waits for the JavaScript bundle to hydrate. Here is how to attack both.

Preload your LCP image

Do not lazy load the image that is your LCP. That is one of the most common mistakes. Preload it instead:

// In your HTML head or via react-helmet-async
<link 
  rel="preload" 
  as="image" 
  href="/hero.webp" 
  fetchpriority="high"
/>

And on the image element itself:

<img 
  src="/hero.webp" 
  alt="Hero" 
  width="1200" 
  height="600"
  fetchpriority="high"
  decoding="async"
/>

Code splitting with React.lazy

Heavy routes shipped in your main bundle will delay the LCP. Split them:

import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';

const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));

export default function App() {
  return (
    <Suspense fallback={<Skeleton />}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </Suspense>
  );
}

Lazy load below-the-fold components

const HeavyChart = lazy(() => import('./HeavyChart'));

function Page() {
  return (
    <>
      <Hero />
      <Suspense fallback={<div style={{ height: 400 }} />}>
        <HeavyChart />
      </Suspense>
    </>
  );
}

Notice the placeholder has a fixed height. That is intentional and ties directly into our next section.

Step 3: Fixing CLS in React

Layout shifts in React almost always come from four sources:

  1. Images without explicit width and height
  2. Web fonts swapping in after render
  3. Async content (ads, embeds, fetched data) injected without reserved space
  4. Conditional rendering that pushes content down

Always reserve space

// BAD: CLS spike when image loads
<img src="/avatar.jpg" alt="User" />

// GOOD: dimensions reserve the space
<img 
  src="/avatar.jpg" 
  alt="User" 
  width="64" 
  height="64" 
  style={{ aspectRatio: '1 / 1' }}
/>

Skeletons must match real content size

function UserCard({ user }) {
  if (!user) {
    // Match the final dimensions exactly
    return <div style={{ height: 120, width: '100%' }} />;
  }
  return <div style={{ height: 120 }}>{user.name}</div>;
}

Fix font swap shifts

/* In your CSS */
@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter.woff2') format('woff2');
  font-display: optional; /* or 'swap' with size-adjust */
  size-adjust: 100%;
}
react performance dashboard

Step 4: Fixing INP in React

INP is where most React apps quietly fail. A single slow event handler or a heavy re-render can tank the score. Here is what works.

Use useTransition for non-urgent updates

import { useState, useTransition } from 'react';

function SearchBox({ items }) {
  const [query, setQuery] = useState('');
  const [filtered, setFiltered] = useState(items);
  const [isPending, startTransition] = useTransition();

  function handleChange(e) {
    const value = e.target.value;
    setQuery(value); // urgent: input must respond immediately
    
    startTransition(() => {
      // non-urgent: filtering can be deferred
      setFiltered(items.filter(i => i.name.includes(value)));
    });
  }

  return (
    <>
      <input value={query} onChange={handleChange} />
      {isPending ? <Spinner /> : <List items={filtered} />}
    </>
  );
}

Break up long tasks

If a click triggers a 400ms synchronous calculation, INP will fail. Yield to the browser:

async function processInChunks(items, chunkSize = 50) {
  for (let i = 0; i < items.length; i += chunkSize) {
    processChunk(items.slice(i, i + chunkSize));
    // Yield to the main thread
    await new Promise(r => setTimeout(r, 0));
  }
}

Memoize expensive children

import { memo, useMemo } from 'react';

const ExpensiveRow = memo(function ExpensiveRow({ data }) {
  return <div>{/* heavy render */}</div>;
});

function Table({ rows, filter }) {
  const visible = useMemo(
    () => rows.filter(r => r.name.includes(filter)),
    [rows, filter]
  );
  return visible.map(r => <ExpensiveRow key={r.id} data={r} />);
}

Step 5: Verify in Lighthouse and Real User Monitoring

After each change, do this loop:

  • Run Lighthouse in incognito mode to avoid extensions skewing results
  • Run it at least 3 times and take the median
  • Compare lab data with field data from the Chrome User Experience Report via PageSpeed Insights
  • Watch the web-vitals events in production for 7 to 28 days before declaring victory
react performance dashboard

Quick Checklist Before You Ship

  • LCP image has fetchpriority="high" and is preloaded
  • Routes use React.lazy with Suspense
  • All images have width and height attributes
  • Skeletons match real content dimensions
  • Fonts use font-display: optional or size-adjust
  • Input handlers use useTransition for heavy work
  • Long synchronous tasks are chunked
  • Third-party scripts load with defer or after interaction

FAQ

Does React’s framework choice affect Core Web Vitals?

Yes. Plain client-side React (Vite, CRA) typically struggles more with LCP than SSR frameworks like Next.js or Remix because the HTML arrives empty. If you’re starting fresh and CWV matters, consider an SSR or SSG framework.

Is FID still measured in 2026?

No. INP officially replaced FID as a Core Web Vital in March 2024. If your monitoring tool still reports FID, update it.

Why does my Lighthouse score change between runs?

Lighthouse simulates a throttled network and CPU. Variability is normal. Always run multiple times, use incognito, and trust field data (CrUX) over lab data for ranking purposes.

Should I lazy load everything?

No. Never lazy load the LCP element or anything in the initial viewport. Lazy loading the hero image is one of the most common ways to make LCP worse, not better.

How long until Google sees my improvements?

CrUX data is a 28-day rolling average, so expect 2 to 4 weeks before your improvements are fully reflected in Search Console and Page Experience reports.

Wrapping Up

Fixing Core Web Vitals on a React website is rarely about one big change. It is about a series of small, measurable wins: a preloaded image here, a useTransition there, a properly sized skeleton in the right place. Run Lighthouse, fix one metric, measure again, repeat.

At Pixelseed, we audit and optimize React applications for performance and SEO every week. If your scores are still red after working through this guide, get in touch and we’ll take a look.

Subscription Form

Contact Details

Quick Links

Copyright © 2022 Pixel Seed. All Rights Reserved.