Skip to main content

Optimize React for LCP: Code-Split & Lazy Load

Optimizing Largest Contentful Paint in React requires two strategies: reduce the size and priority of above-the-fold content, and defer below-the-fold code and images. Code-splitting separates your bundle into smaller chunks that load on demand; lazy loading defers image downloads until they enter the viewport; optimized fonts avoid invisible text during loading. Combined, these techniques reduce LCP from 4+ seconds to under 2.5 seconds for typical e-commerce and blog applications.

Code-Splitting in React with React.lazy()

Code-splitting breaks your JavaScript bundle into smaller files, so users only download code for the routes and components they visit. In React, use React.lazy() and Suspense to load components on demand:

// src/App.jsx
import React, { Suspense, lazy } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';

// Route-based code splitting
const Home = lazy(() => import('./pages/Home'));
const ProductList = lazy(() => import('./pages/ProductList'));
const ProductDetail = lazy(() => import('./pages/ProductDetail'));
const Checkout = lazy(() => import('./pages/Checkout'));

// Loading fallback component
function LoadingSpinner() {
return <div className="spinner" />;
}

export default function App() {
return (
<BrowserRouter>
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/products" element={<ProductList />} />
<Route path="/products/:id" element={<ProductDetail />} />
<Route path="/checkout" element={<Checkout />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}

When the user navigates to /products, React downloads only the ProductList chunk. Chunks are cached by the browser and the bundler (webpack, Vite, etc.) so repeat visits are instant. By default, your home page bundle shrinks from 500 KB to 150 KB, cutting LCP by 40% on slow networks (Web.dev, 2024).

Lazy Loading Images Above the Fold

Images are often the LCP candidate in React apps. Use native HTML loading="lazy" for below-the-fold images, and for above-the-fold images, use fetchpriority="high" to hint the browser to prioritize their download:

// src/components/HeroSection.jsx
export default function HeroSection() {
return (
<section className="hero">
{/* Above the fold: hero image */}
<img
src="/images/hero.jpg"
alt="Our product in action"
width="1200"
height="600"
fetchpriority="high"
// ^ tells browser: download this before lower-priority images
/>

{/* Below the fold: lazy load */}
<section className="features">
<img
src="/images/feature1.jpg"
alt="Feature 1"
width="400"
height="300"
loading="lazy"
// ^ download only when near viewport
/>
<img
src="/images/feature2.jpg"
alt="Feature 2"
width="400"
height="300"
loading="lazy"
/>
</section>
</section>
);
}

Always set width and height on images; this prevents Cumulative Layout Shift (CLS) when the image loads. fetchpriority="high" is especially effective for hero images—it reduces LCP by 200–500 ms on slow networks (Chromium Blog, 2025).

Responsive Images with srcset

Use srcset to serve appropriately sized images to different devices and screen densities. This reduces the bytes downloaded and speeds up LCP on mobile devices:

// src/components/ResponsiveImage.jsx
export default function ResponsiveImage({ alt }) {
return (
<img
alt={alt}
fetchpriority="high"
width="1200"
height="600"
src="/images/hero-800w.jpg"
srcSet="
/images/hero-480w.jpg 480w,
/images/hero-800w.jpg 800w,
/images/hero-1200w.jpg 1200w,
/images/hero-1920w.jpg 1920w
"
sizes="
(max-width: 480px) 480px,
(max-width: 800px) 800px,
(max-width: 1200px) 1200px,
1920px
"
/>
);
}

The browser now downloads only the image size that fits the user's device. A mobile phone on a slow 4G network downloads the 480-pixel version (20–30 KB) instead of the 1200-pixel version (100 KB), cutting LCP by 30–40%.

Resource Hints: preconnect, prefetch, preload

Use resource hints in the <head> to prioritize critical resources:

<!-- public/index.html -->
<head>
<!-- Preconnect to third-party origins (fonts, APIs) -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preconnect" href="https://api.example.com">

<!-- Prefetch next page's code chunk (if using route-based splitting) -->
<link rel="prefetch" href="/js/ProductList.chunk.js">

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

<!-- Load font stylesheet -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap" rel="stylesheet">
</head>
  • preconnect: Opens a TCP connection to a third-party server early (saves 100–300 ms).
  • prefetch: Hints the browser to download a resource in the background (for next-page navigations).
  • preload: Forces the browser to download a critical resource immediately (use sparingly; over-preloading slows LCP).

In practice, preconnecting to a fonts CDN and preloading the primary font file reduces LCP by 200 ms (Web.dev, 2025).

Font Loading Strategy: font-display

Fonts are render-blocking if not optimized. Use font-display: swap to show fallback text immediately while the custom font downloads:

/* src/styles/fonts.css */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var.woff2') format('woff2');
font-display: swap; /* Show fallback text, swap in custom font when ready */
font-weight: 100 900;
font-style: normal;
}

Options for font-display:

  • auto (default): browser decides; often blocks text rendering for up to 3 seconds.
  • swap: show fallback immediately, replace with custom font when ready (best for most sites).
  • fallback: short invisible period (100 ms), then fallback for 3 seconds, then swap.
  • optional: browser may skip custom font if it loads slowly (for non-critical fonts).

font-display: swap keeps LCP low because text renders immediately, not blocked by font downloads.

Deferring Non-Critical JavaScript

Move analytics, error tracking, and third-party scripts out of the critical path. Use the async or defer attribute on script tags:

<!-- public/index.html -->
<body>
<div id="root"></div>

<!-- React app: critical, loaded synchronously in production build -->
<script type="module" src="/src/main.jsx"></script>

<!-- Non-critical: load after page is interactive -->
<script async src="https://www.googletagmanager.com/gtag/js?id=GA_ID"></script>
<script src="https://cdn.sentry.io/..."></script>
</body>
  • async: Download in parallel, execute as soon as ready (no ordering guarantee).
  • defer: Download in parallel, execute after HTML parsing completes (preserves script order).
  • Neither: synchronous, blocks HTML parsing (avoid unless critical).

Deferring non-critical scripts can reduce LCP by 300–800 ms.

Checking LCP Improvements with Lighthouse

After optimizing, run Lighthouse to measure LCP improvement. In Chrome DevTools, Lighthouse's "Largest Contentful Paint" row shows the time and the element responsible. Opportunities section lists specific optimizations like "unused CSS" or "unused JavaScript."

Compare before/after LCP scores:

BeforeAfterImprovement
4.2s2.1s50%
PoorGood+Ranking lift

A 50% reduction takes a page from "poor" to "good" and enables the Google search "Core Web Vitals" badge.

Key Takeaways

  • Code-splitting with React.lazy() and Suspense reduces initial bundle size and LCP.
  • Set fetchpriority="high" on above-the-fold images to prioritize their download.
  • Always set width and height on images to prevent layout shift.
  • Use srcset and sizes for responsive images; mobile users download smaller files.
  • Preconnect to third-party origins and preload critical fonts to save 100–300 ms.
  • Use font-display: swap to show fallback text while custom fonts load.
  • Defer analytics and third-party scripts with async or defer attributes.

Frequently Asked Questions

Does React.lazy() add extra overhead?

No. React.lazy() uses dynamic import(), which is handled by your bundler at build time. The overhead is minimal (a few milliseconds) and the bundle size savings (40–50%) far outweigh it. Always use code-splitting for route-based pages.

Should I preload all images on the page?

No. Preload only critical above-the-fold images. Preloading too many resources defeats the purpose and slows LCP. Use loading="lazy" for below-the-fold images and fetchpriority="high" for hero images.

How do I know which images are slowing down LCP?

Use Chrome DevTools' Network tab and filter by images. Sort by "Start time" to see which images downloaded first. The image with the longest download time that is also visible above the fold is likely your LCP candidate. PerformanceObserver (from article 2) reveals the exact element.

Can I use a CDN or image service to optimize images?

Yes. Services like Cloudflare Image Optimization, Imgix, or Vercel OG Images automatically optimize images for device and network. These services resize, compress, and serve WebP/AVIF formats, reducing image bytes by 30–60% and LCP by 200–500 ms.

Further Reading