How to lazy load images in React without hurting LCP
To lazy load an image in React, render a normal img element with loading="lazy". Keep the real
URL in src, add srcSet and sizes when responsive variants are available, and provide width
and height so the browser can reserve space.
The important exception is the image likely to become the page's Largest Contentful Paint (LCP) element. Keep that image eager and discoverable in the initial HTML. Lazy loading it delays the request that the page most needs to finish quickly.
| Image role | Loading decision | Other important markup |
|---|---|---|
| Hero or likely LCP image | Eager | fetchPriority="high" when verified |
| Other image visible in the first viewport | Eager | Dimensions and responsive sources |
| Ordinary offscreen content image | Lazy | src, srcSet, sizes, and dimensions |
| Decorative CSS background | Separate mechanism | Do not replace meaningful img content |
“Above the fold” is not a fixed image number. Viewport height, responsive layouts, banners, and localized content can move the same image into or out of the initial viewport. Classify images by the rendered page, then verify the choice on representative screen sizes.
Use React's native loading prop
React passes the loading prop to the browser's native image element. You do not need an effect,
scroll handler, or Intersection Observer library for an ordinary offscreen image.
import type { ReactNode } from 'react'
interface ResponsiveImageProps {
alt: string
height: number
sizes: string
src: string
srcSet: string
width: number
}
export function ResponsiveImage({
alt,
height,
sizes,
src,
srcSet,
width,
}: ResponsiveImageProps): ReactNode {
return (
<img
className="responsive-image"
src={src}
srcSet={srcSet}
sizes={sizes}
width={width}
height={height}
loading="lazy"
decoding="async"
alt={alt}
/>
)
}
Keep an actual image URL in src. Moving the only URL into a custom data-src attribute makes the
request depend on your JavaScript and prevents the browser's preload scanner from discovering it in
normal image markup. It also leaves the image unloaded if the script fails.
The decoding="async" prop is a separate hint about decoding. It does not decide when the network
request starts and it does not replace loading="lazy".
Use the component in a list just as you would use any other React component:
<ResponsiveImage
src="/images/workshop-960.webp"
srcSet="/images/workshop-480.webp 480w, /images/workshop-960.webp 960w"
sizes="(max-width: 40rem) 100vw, 40rem"
width={960}
height={640}
alt="A technician calibrating a camera rig"
/>
Lazy loading only changes when the request starts. It does not resize, compress, convert, cache, or describe the image.
Keep the LCP image eager
Do not apply loading="lazy" to a likely LCP image. Render its URL in the initial HTML when your
React framework supports server rendering so the browser can discover it without waiting for
client-side rendering or another data request.
import type { ReactNode } from 'react'
export function HeroImage(): ReactNode {
return (
<img
className="responsive-image"
src="/images/harbor-1280.webp"
srcSet="/images/harbor-640.webp 640w, /images/harbor-1280.webp 1280w"
sizes="100vw"
width={1280}
height={720}
loading="eager"
fetchPriority="high"
alt="Fishing boats returning to the harbor at sunrise"
/>
)
}
fetchPriority="high" is a relative priority hint, not a guarantee about request order. Reserve it
for the measured LCP image. If several images are marked high priority, the browser has less useful
information for deciding which one matters most.
An image can be the LCP element on mobile but not desktop, or the reverse. Use field performance data to confirm which element becomes LCP for real visitors instead of assuming that every large image or carousel slide is critical.
Combine lazy loading with responsive images
A delayed request can still download a needlessly large file. With width descriptors such as
480w, srcSet supplies the candidate files and sizes describes the width of the rendered slot.
The browser combines that information with the device pixel density and chooses a candidate.
Write sizes for the layout, not for the source file. If an image occupies the full viewport on a
phone and a 640-pixel column on wider screens, say so:
<img
className="responsive-image"
src="/images/product-960.webp"
srcSet="/images/product-480.webp 480w, /images/product-960.webp 960w"
sizes="(max-width: 40rem) 100vw, 40rem"
width={960}
height={640}
loading="lazy"
decoding="async"
alt="Red trail shoe viewed from the side"
/>
The browser runs the same responsive candidate selection for eager and lazy images. The loading
decision does not remove the need for correct candidates and an accurate sizes value.
Reserve space to avoid layout shifts
Supply width and height for every content image. Browsers use those attributes to calculate an
aspect ratio before the image has downloaded, reserving the right amount of layout space. Your CSS
can still make the image fluid:
.responsive-image {
display: block;
height: auto;
max-width: 100%;
}
The attributes must describe the image's intrinsic aspect ratio. For example, an img can use
width={960} and height={640} when every candidate has a 3:2 ratio, even if the browser selects a
480×320 file. If art-directed sources use different crops or aspect ratios, their dimensions need
to describe the selected source rather than an unrelated fallback.
Reserved image space prevents one source of Cumulative Layout Shift (CLS), not every possible shift. Captions, ads, consent controls, fonts, and error messages can still move the page.
When Intersection Observer is appropriate
Use the browser's native loading behavior for ordinary images. Reach for Intersection Observer only
when the component needs behavior that loading="lazy" cannot express, such as starting an
animation, recording a visibility event, or applying an expensive decorative background shortly
before it reaches the viewport.
Do not conditionally mount a normal content image only after an observer fires unless that behavior
is genuinely required. Conditional mounting delays discovery, adds a JavaScript failure path, and
requires you to design a stable fallback. Meaningful images should remain img or picture
elements with useful alternative text.
React.lazy() solves a different problem: it defers loading a JavaScript component module. It does
not automatically defer image requests rendered by that component.
Generate responsive derivatives with Transloadit
Lazy loading cannot repair an oversized source image. A practical delivery pipeline generates a small, bounded set of widths, optimizes each result, exports them to durable storage, and records their URLs and dimensions for the React view.
Transloadit's /image/resize Robot can create those width variants, and the /image/optimize Robot can optimize the supported results:
{
"steps": {
"small": {
"robot": "/image/resize",
"use": ":original",
"width": 480,
"resize_strategy": "fit",
"zoom": false,
"format": "webp"
},
"medium": {
"robot": "/image/resize",
"use": ":original",
"width": 960,
"resize_strategy": "fit",
"zoom": false,
"format": "webp"
},
"large": {
"robot": "/image/resize",
"use": ":original",
"width": 1440,
"resize_strategy": "fit",
"zoom": false,
"format": "webp"
},
"small_optimized": {
"robot": "/image/optimize",
"use": "small"
},
"medium_optimized": {
"robot": "/image/optimize",
"use": "medium"
},
"large_optimized": {
"robot": "/image/optimize",
"use": "large"
}
}
}
The example uses zoom: false so a small upload is not enlarged merely to reach a target width.
After processing, add a storage Robot or another export step appropriate to your architecture, then
use those durable delivery URLs in src and srcSet. Assembly result URLs are temporary processing
outputs, not permanent application storage. See the file exporting
service for the available storage integrations. Do not wait for the
React component to create derivatives. If you prefer to transform and cache approved widths on
demand, see the guide to serving responsive images from one
URL.
The React application still owns presentation decisions: which image is meaningful, its alternative text, its rendered slot size, and whether it is initially critical. Transloadit handles the media processing stage rather than deciding which image becomes LCP in a particular page layout.
Measure the result
Check the production page rather than relying only on the component source:
- Record narrow and wide viewport loads with a cold cache in browser developer tools.
- Confirm that the LCP image request starts early and is not marked lazy.
- Scroll and verify that ordinary offscreen images are deferred without appearing late to the user.
- Inspect the selected
srcSetcandidate and compare its dimensions with the rendered slot. - Measure LCP and CLS with field data, grouped by page template and relevant viewport.
- Block image requests to verify useful alternative text and stable failure states. Where the page supports server rendering, also inspect the server-rendered result before hydration.
Browser lazy-loading distances and scheduling are implementation-dependent. Avoid a rule such as “lazy load everything after the third image” or an assumed fixed pixel threshold. Measure the pages and devices your users actually receive.
Common React lazy-loading mistakes
- Lazy loading the hero or measured LCP image.
- Moving the only image URL from
srctodata-src. - Rendering critical images only after client-side code or a data request completes.
- Supplying
srcSetwithout an accuratesizesvalue. - Omitting
widthandheightbecause CSS eventually controls the image size. - Marking every image with
fetchPriority="high". - Using an observer library only to reproduce native browser behavior.
- Assuming
React.lazy()controls image network requests. - Deferring a 3,000-pixel source instead of generating a suitable derivative.
The reliable default is simple: render complete image markup, keep likely LCP images eager, lazy load ordinary offscreen images, and make every requested file appropriate for the slot that displays it.
For the underlying browser rules, see the HTML lazy-loading standard, web.dev's LCP guidance, and its guide to responsive images.
