How to lazy load images in React
Lazy loading defers image loading until they enter the viewport, dramatically improving initial page load times and reducing bandwidth consumption for users. As the creator of CoreUI, a widely used open-source UI library, I’ve implemented lazy loading in media-heavy applications throughout my 12 years of frontend development since 2014. The most efficient approach is combining native HTML loading=‘lazy’ attribute with Intersection Observer API for browsers that need polyfill support. This method provides optimal performance, progressive enhancement, and graceful degradation across different browser capabilities.
Use native loading=‘lazy’ attribute for modern browsers with Intersection Observer fallback for comprehensive support.
import { useState, useEffect, useRef } from 'react'
const LazyImage = ({ src, alt, placeholder }) => {
const [imageSrc, setImageSrc] = useState(placeholder)
const [isLoaded, setIsLoaded] = useState(false)
const imgRef = useRef(null)
useEffect(() => {
if ('loading' in HTMLImageElement.prototype) {
setImageSrc(src)
return
}
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
setImageSrc(src)
observer.unobserve(entry.target)
}
})
},
{ rootMargin: '50px' }
)
if (imgRef.current) {
observer.observe(imgRef.current)
}
return () => {
if (imgRef.current) {
observer.unobserve(imgRef.current)
}
}
}, [src])
return (
<img
ref={imgRef}
src={imageSrc}
alt={alt}
loading='lazy'
onLoad={() => setIsLoaded(true)}
style={{
opacity: isLoaded ? 1 : 0.5,
transition: 'opacity 0.3s ease-in-out'
}}
/>
)
}
const ImageGallery = ({ images }) => {
return (
<div className='gallery'>
{images.map(image => (
<LazyImage
key={image.id}
src={image.url}
alt={image.alt}
placeholder='/placeholder.jpg'
/>
))}
</div>
)
}
Using react-lazyload library for advanced lazy loading with more control:
npm install react-lazyload
import LazyLoad from 'react-lazyload'
const LazyImageWithLibrary = ({ src, alt, height }) => {
const [isLoading, setIsLoading] = useState(true)
return (
<LazyLoad height={height} offset={100} once>
<div style={{ position: 'relative' }}>
{isLoading && (
<div style={{
position: 'absolute',
width: '100%',
height: '100%',
background: '#f0f0f0',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
Loading...
</div>
)}
<img
src={src}
alt={alt}
onLoad={() => setIsLoading(false)}
style={{ width: '100%', height: 'auto' }}
/>
</div>
</LazyLoad>
)
}
const OptimizedGallery = ({ images }) => {
return (
<div>
{images.map(image => (
<LazyImageWithLibrary
key={image.id}
src={image.url}
alt={image.alt}
height={300}
/>
))}
</div>
)
}
Here the loading=‘lazy’ attribute provides native browser lazy loading for Chrome, Firefox, and Safari without JavaScript overhead. The IntersectionObserver API detects when images enter viewport with 50px margin for preloading. The useEffect hook checks browser support for native lazy loading and falls back to observer-based approach. The isLoaded state controls opacity transition for smooth visual loading effect. The react-lazyload library provides advanced features like placeholder height to prevent layout shift. The offset prop preloads images 100px before viewport entry for seamless user experience. The once prop ensures images load only one time and stay loaded for better performance.
Best Practice Note:
This is the lazy loading strategy we use in CoreUI React templates for gallery components and media-heavy admin dashboards with thousands of images. Add blur-up placeholder technique by loading tiny base64-encoded thumbnails first, implement error boundaries to handle failed image loads gracefully, and use responsive images with srcset attribute combined with lazy loading for optimal bandwidth usage across different device sizes and pixel densities.



