EDITED: From Chrome 76 onwards, Chrome supports native lazy loading for images 🎉🎉🎉! Example: <img loading="lazy" />
Images have always been one of the primary culprits for slow websites. Loading all images at once not only affects the load time but also exhausts users’ unnecessary bandwidth. One of the solutions for optimising images is Lazy Loading. In other words, websites should delay fetching images from their source before they are in the view of the users. https://web.dev/browser-level-image-lazy-loading/ Today I am going to share with you guys a simple approach to lazy load images in your React apps with IntersectionObserver API and React Hooks.
IntersectionObserver is a web API that allows developers to detect visibility of an element, or the relative visibility of two elements in relation to each other.
Note : IntresectionObserver API is not available for all browsers. Check caniuse for more details.
Hooks is a new feature which allows developers to upgrade their functional components with lifecycles, states and many more without transforming the dumb components to classes. Check out my blog for a brief introduction to React Hooks.
Function to create a new Observer
function createObserver(inViewCallback = noop, newOptions = {}) {
const defaultOptions = {
root: null,
rootMargin: '0px',
threshold: 0.3,
}
return new IntersectionObserver(inViewCallback, Object.assign(defaultOptions, newOptions));
}
This function returns an instance of IntersectionObserver. The class’s constructor takes two parameters:
options object
null
or it is not specified.Image Component
import React, {useEffect, useRef} from 'react';
const LazyImage = ({
observer,
src,
alt,
}) => {
const imageEl = useRef(null);
useEffect(() => {
const {current} = imageEl;
if (observer !== null) {
observer.observe(current);
}
return () => {
observer.unobserve(current);
}
}, [observer]);
return (
<img
ref={imageEl}
data-src={src}
alt={alt}
/>
)
}
export default LazyImage;
LazyImage component
useEffect
and useRef
. useEffect
is used to mimic the effects of componentDidMount
which is, in our use case, to ensure imageEl
has the element’s reference before the img
element is observed.useEffect
function, we use an Observer which is passed as a prop to observe the image element. The image element is retrieve as a reference with the useRef
function.useEffect
will stop being called only after the Observer prop has a value.Usage
import React, {useEffect, useState} from 'react';
import LazyImage from './LazyImage';
function onImageInView(entries, observer) {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const element = entry.target;
const imageSrc = element.getAttribute('data-src');
element.removeAttribute('data-src');
element.setAttribute('src', imageSrc);
observer.unobserve(element);
}
});
}
const Parent = ({images}) => {
const [imageObserver, setImageObserver] = useState(null);
useEffect(() => {
const imageObserver = createObserver(onImageInView);
setImageObserver(imageObserver);
return () => {
imageObserver.disconnect();
}
}, []);
return images.map(({src, alt}, index) => (
<LazyImage key={index} observer={imageObserver} src={src} alt={alt} />
))
}
Let’s break it down:
onImageView function
isIntersecting
field.Parent component
useEffect
function, we initialise our observer and store it to a state variable using a function provided by the useState
function.useEffect
function is being called every time the Parent
component is re-rendered. LazyImage
component.