Fix LCP in React
Largest Contentful Paint is the hardest Core Web Vital to optimize in client-rendered React applications because the LCP element is typically rendered by JavaScript -- meaning the browser must download, parse, and execute your bundle before it can paint the element. This guide covers the highest-impact fixes, from image preloading to server-side rendering, with practical code examples.
Expected results
Before
3.8s
LCP (Poor) -- client-side render, no image preload, large bundle
After
1.7s
LCP (Good) -- SSR with image preload, code splitting, inline critical CSS
Step-by-step fix
Preload the LCP image
In a client-side React app, the LCP image is often discovered late because it is injected by JavaScript after the bundle executes. Add a <link rel="preload"> tag for the LCP image directly in your HTML template so the browser fetches it at the earliest possible moment -- before React even starts.
<!-- In your public/index.html <head> -->
<!-- Preload the hero image so browser fetches it immediately -->
<link
rel="preload"
as="image"
href="/images/hero.webp"
type="image/webp"
fetchpriority="high"
/>
<!-- For React apps using react-helmet-async -->
import { Helmet } from 'react-helmet-async';
function HeroSection() {
return (
<>
<Helmet>
<link
rel="preload"
as="image"
href="/images/hero.webp"
fetchpriority="high"
/>
</Helmet>
<img src="/images/hero.webp" alt="Hero" width={1200} height={630} />
</>
);
}
Add SSR or static rendering
The most impactful LCP improvement for React apps is moving the LCP element out of client-side JavaScript and into server-rendered HTML. Pure CSR (create-react-app style) forces the browser to run JS before anything renders. With SSR, the browser paints immediately from the HTML stream.
// server.js -- Basic React SSR
import express from 'express';
import { renderToString } from 'react-dom/server';
import App from './src/App';
const app = express();
app.get('*', (req, res) => {
// HTML is sent immediately -- LCP element is in the initial payload
const html = renderToString(<App url={req.url} />);
res.send(`
<!DOCTYPE html>
<html>
<head>
<link rel="preload" as="image" href="/hero.webp" />
<link rel="stylesheet" href="/styles.css" />
</head>
<body>
<div id="root">${html}</div>
<script src="/bundle.js" defer></script>
</body>
</html>
`);
});
// Or migrate to Next.js / Remix which handle this automatically
Reduce JavaScript bundle size
Every kilobyte of JavaScript delays rendering. Use React.lazy() and Suspense to code-split routes and below-the-fold components. Analyze the bundle with webpack-bundle-analyzer to identify large dependencies. Common wins: replacing moment.js with date-fns, removing polyfills for modern browsers, and tree-shaking lodash.
import React, { lazy, Suspense } from 'react';
// Split routes -- each loads separately
const HomePage = lazy(() => import('./pages/HomePage'));
const DashboardPage = lazy(() => import('./pages/DashboardPage'));
const SettingsPage = lazy(() => import('./pages/SettingsPage'));
// Split heavy below-fold components
const CommentsSection = lazy(() => import('./components/Comments'));
const RecommendationsGrid = lazy(() => import('./components/Recommendations'));
function App() {
return (
<Router>
<Suspense fallback={<PageSkeleton />}>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/dashboard" element={<DashboardPage />} />
</Routes>
</Suspense>
</Router>
);
}
Set image dimensions to prevent CLS that delays LCP
Images without dimensions cause CLS (layout shift) which forces re-layout and can reset or delay LCP measurement. Always include width and height attributes on every <img> element, and pair with height: auto in CSS for responsive sizing.
// Always include width and height to prevent layout shifts
function HeroImage() {
return (
<img
src="/hero.webp"
alt="Product showcase"
width={1200}
height={630}
// CSS makes it responsive while browser reserves correct space
style={{ width: '100%', height: 'auto' }}
// fetchpriority tells browser to prioritize this image
fetchPriority="high"
/>
);
}
// For background-image LCP elements, use a real img tag instead
// Background images have lower priority and are discovered later
Minimize render-blocking CSS
CSS that loads in <link> tags blocks rendering. Only the critical CSS needed for above-the-fold content should block render; everything else should be loaded asynchronously. Extract critical CSS inline and load the full stylesheet asynchronously.
<!-- Critical CSS inline in <head> -- blocks render but small -->
<style>
/* Only styles needed for above-the-fold content */
body { margin: 0; font-family: system-ui, sans-serif; }
.hero { position: relative; aspect-ratio: 16/9; }
.hero img { width: 100%; height: auto; }
.nav { height: 60px; background: #0f172a; }
</style>
<!-- Full stylesheet loaded async -- does not block LCP -->
<link
rel="stylesheet"
href="/styles.css"
media="print"
onload="this.media='all'"
/>
<noscript><link rel="stylesheet" href="/styles.css"></noscript>
Quick checklist
-
LCP image has
fetchpriority="high"or a<link rel="preload"> - App uses SSR, SSG, or at minimum inline-renders the LCP element in the HTML
-
All images have
widthandheightattributes -
Below-the-fold components use
React.lazy() - Critical CSS is inline or render-blocking stylesheet is minimal
-
Third-party scripts use
deferorasyncattribute
Frequently asked questions
React SPAs have poor LCP because the LCP element is rendered client-side -- the browser must first download, parse, and execute JavaScript before React renders anything. LCP is blocked by the full JS load time. Solutions: SSR, static rendering, or ensuring the LCP element is in the server-rendered HTML. Moving to SSR typically cuts LCP by 50-70%.
Yes, significantly. With SSR the browser receives rendered HTML immediately and the LCP element is painted before any JavaScript loads. React then hydrates the HTML, but LCP is already captured. Moving from a pure CSR SPA to SSR typically cuts LCP by 50-70%.
Add a <link rel="preload" as="image" href="/path/to/image.webp"> tag in the <head> of your HTML template. In React apps, use react-helmet-async or inject it in your index.html. The browser starts fetching before the React component tree renders.
Yes, if used on above-the-fold components. React.lazy() delays loading the component until needed -- if the LCP element is inside a lazy-loaded component, LCP will be poor because the browser waits for the lazy bundle. Only use React.lazy() for below-the-fold or on-demand content.
With SSR or static rendering, aim for under 2.0s. With client-side-only rendering, even highly optimized apps rarely achieve under 2.5s due to JavaScript execution overhead. Google's good threshold is 2.5s at the 75th percentile -- CSR React apps frequently fail this without SSR or prerendering.
Google rates LCP as 'good' when it is under 2.5 seconds at the 75th percentile. For React applications specifically, aim for under 2.0 seconds. Measure with field data from Chrome User Experience Report (CrUX) through PageSpeed Insights, as lab tests may not reflect real-user experience with third-party scripts and varying network conditions.
Related resources
Complete LCP Guide
Deep dive into LCP calculation, measurement tools, and universal optimization techniques.
FixFix LCP in Next.js
Next.js-specific optimizations including next/image priority, ISR, and Server Components.
FixFix CLS in React
Eliminate layout shifts from images, fonts, and dynamic content in React applications.
Continue learning
Complete LCP Guide
Deep dive into LCP -- thresholds, measurement, and optimization strategies.
FixFix CLS in React
Related performance optimization for the same framework.
FixFix INP in React
Related performance optimization for the same framework.
ToolCWV Score Explainer
Enter your scores for personalized fix recommendations.