Lazy loading images with Intersection Observer in React
Mileta Dulovic
Let's see how we can make custom image components that will handle loading the image lazily and improve website performance
significantly, by using IntersectionObserver
. We will do this in React
, but same logic can be used in any JavaScript
framework.
Intersection Observer
If you don't know what Intersection Observer is here is the definition from MDN
The Intersection Observer API provides a way to asynchronously observe changes in the intersection of a target element with an ancestor element or with a top-level document's viewport.
In other words, you can detect when element intersects with other elements or the viewport (when it leaves or enters viewport).
Here is how we initialize Intersection Observer
in JavaScript
const observer = new IntersectionObserver(onIntersection, {
root: null, // default is the viewport
threshold: 0.5, // percentage of target's visible area. Triggers "onIntersection"
});
As you can see root is null, and that is because in React we don't access elements directly in the DOM
, but rather we use refs
, like this.
<img
{...props}
ref={ref}
/>
Now that we assigned ref to the element we can connect it to Intersection Observer
like this
observer.observe(ref.current);
Once we connect the element, onIntersection
function will be triggered every time element enters the viewport. Function looks something like this:
function onIntersection(entries, opts) {
entries.forEach((entry) => {
if (entry.isIntersecting) {
// if element is in viewport do something
}
});
}
Okay, so now that you know what Intersection Observer is let's continue.
Adding placeholer images
Instead of loading image immediately on site load, we will add placeholder element (can be anything but we will use placeholder image) that will be used as a trigger for when it reaches viewport.
<img
{...props}
ref={placeholderRef}
src="/placeholder.png"
alt={props.alt || ""}
/>
Note that placeholder image has ref
that we will pass to IntersectionObserver
.
Final code
When we connect all the code pieces and logic from above we get this component that we can use as a drop-in <img />
replacement in our website.
import { useEffect, useRef, useState } from "react";
const LazyImage = (props) => {
const [inView, setInView] = useState(false);
const placeholderRef = useRef();
function onIntersection(entries, opts) {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setInView(true);
}
});
}
useEffect(() => {
const observer = new IntersectionObserver(onIntersection, {
root: null, // default is the viewport
threshold: 0.5, // percentage of target's visible area. Triggers "onIntersection"
});
if (placeholderRef?.current) {
observer.observe(placeholderRef.current);
}
return () => {
observer.disconnect();
};
}, []);
return inView ? (
<img {...props} alt={props.alt || ""} />
) : (
<img
{...props}
ref={placeholderRef}
src="/placeholder.png"
alt={props.alt || ""}
/>
);
};
export default LazyImage;
Conclusion
This is simple way to improve website performance, and provide our users with beter experience while browsing.
We will cover more ways to optimize and improve website performance in future posts, so stay tuned.
More in this category
JavaScript Loops performance
Yet another post that iterates over items to measure performance of different loops in JavaScript.
How to cancel requests with Apollo GraphQL
If you need to cancel requests with Apollo Graphql in React, and do not know how to do it, you came to the right place.
Infinite scroll component in React and NextJS
Infinite scroll is a great way to load huge lists as it provides a great UX for user when compared to traditional pagination