Lazy loading in React means fetching code or images only when they are needed. Wrap a dynamic import in lazy(), render the component inside a Suspense boundary with a fallback, and React downloads it on first render. For images, the HTML attribute loading="lazy" does the same, but never on your main above-the-fold picture.
| What to delay | Tool | Where it is documented |
|---|---|---|
| A heavy component | lazy() plus Suspense | react.dev |
| A whole page or route | The route lazy property | reactrouter.com |
| Images below the fold | loading="lazy" on img | MDN |
| Anything when it scrolls into view | IntersectionObserver | MDN |
Updated
A big app bundle makes every visitor wait for code they may never use, such as a chart on a settings tab or an admin page. Lazy loading splits that code into separate files the browser fetches on demand. Try the demo below, then copy the patterns for components, routes, images and scroll-triggered content.
This button renders a component created with lazy(). Its download is faked to take 2 seconds the first time.
The standard pattern: one lazy declaration at the top of the file, and a Suspense boundary around where it renders.
The chart file is not fetched until showChart becomes true for the first time.
import { lazy, Suspense, useState } from "react";
import ErrorBoundary from "./ErrorBoundary";
const SalesChart = lazy(() => import("./SalesChart"));
export default function Dashboard() {
const [showChart, setShowChart] = useState(false);
return (
<>
<button onClick={() => setShowChart(true)}>Show chart</button>
{showChart && (
<ErrorBoundary fallback={<p>Could not load the chart.</p>}>
<Suspense fallback={<div style={{ height: 240 }}>Loading chart...</div>}>
<SalesChart />
</Suspense>
</ErrorBoundary>
)}
</>
);
}React Router route objects take a lazy function that returns the route's properties, so each page becomes its own file.
The reports page and its loader download only when someone visits /reports.
import { createBrowserRouter, RouterProvider } from "react-router";
import Home from "./pages/Home";
const router = createBrowserRouter([
{ path: "/", Component: Home },
{
path: "/reports",
lazy: async () => {
const [page, data] = await Promise.all([
import("./pages/Reports"),
import("./pages/reports-loader"),
]);
return { Component: page.default, loader: data.loader };
},
},
]);
export default function App() {
return <RouterProvider router={router} />;
}Browsers handle image lazy loading natively. Mark below-the-fold images lazy and give the main image a high fetch priority.
Width and height reserve space so nothing shifts when the image arrives.
<!-- Main picture at the top: load straight away -->
<img src="/img/hero.jpg" alt="Our bakery counter" width="1200" height="600" fetchpriority="high">
<!-- Further down the page: wait until the visitor scrolls near it -->
<img src="/img/team.jpg" alt="The team at work" width="800" height="533" loading="lazy">For content that is not an image, such as a map or reviews widget, a small hook tells you when its box enters the screen.
The rootMargin of 200px starts loading a little before the box is visible; once seen, the observer disconnects.
import { useEffect, useRef, useState } from "react";
export function useInView(rootMargin = "200px") {
const ref = useRef(null);
const [inView, setInView] = useState(false);
useEffect(() => {
const el = ref.current;
if (!el) return;
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
setInView(true);
observer.disconnect();
}
}, { rootMargin });
observer.observe(el);
return () => observer.disconnect();
}, [rootMargin]);
return [ref, inView];
}
// Usage: const [ref, inView] = useInView();
// <div ref={ref} style={{ minHeight: 300 }}>{inView && <StoreMap />}</div>
Largest Contentful Paint, often shortened to LCP, measures when the biggest image or text block in view finishes rendering. If that image waits for the lazy loading check, it arrives later.
In web.dev's tests on WordPress archive pages, turning off lazy loading improved LCP, and the best result came from lazy loading only images below the fold.
Small components cost more as separate files than they save, because each one adds a network request. Lazy load things that are large and not needed straight away, such as editors, maps, charts and admin areas.
If a component appears on nearly every visit right after load, splitting it only adds a loading flash.
A component you can render. Its load function must return a Promise that resolves to an object whose default property is a valid React component, which a dynamic import() of a default export does.
No. react.dev says both the returned Promise and its resolved value are cached, so React does not call the load function more than once. The demo on this page shows the second open happening instantly.
lazy() expects a default export. For a named one, map it inside the load function: lazy(() => import("./Charts").then((m) => ({ default: m.SalesChart }))).
Yes, for the largest image on screen. web.dev found that lazy loading above-the-fold images tended to slow Largest Contentful Paint, and that lazy loading only images below the fold kept the data savings without that cost.
React throws the rejection reason during render, and the nearest error boundary catches it. Without one, react.dev says React removes the app's UI from the screen by default.
Related: React performance APIs, React Router examples, How to optimize website speed, Website image sizes.
Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.
We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.