There are times when using a background image is a better option than using an img
tag. For instance, if you want to display an image that's purely for decoration and doesn't convey important information to the user, a background image is a great choice. Also, if you have multiple instances of the same image on a page (like repeating patterns), using a background image is more efficient since it only needs to load once.
In our last post, we learned how to use IntersectionObserver to lazy load an image. In this post, we'll use the same technique to lazy load a background image of an element. Let's dive in!
Updating the background image attribute
When we want to set the background image of an element in CSS, we typically use the background-image
property.
<div
style={{
backgroundImage: `url(...)`
}}
/>
However, to lazy load the background image, we can use a similar approach to lazy loading an image. First, we replace the background-image
style with a custom data attribute, such as data-background-src
. Then, we monitor the intersection of the element. Once the element is visible in the viewport, we reset the background-image
style with the image specified in the custom attribute.
To begin, we use a custom data attribute to show where the background image comes from. It's worth noting that the value of our custom attribute directly stores the image URL, without the url
function found in the original background-image
attribute.
<div
ref={elementRef}
data-background-src={`https://...`}
/>
The elementRef
in React refers to the target element we want to observe. To create an IntersectionObserver
, we use the sample code below with a callback function that takes an array of entries
as its argument. The entries
array contains information about the intersection between the observed element and the viewport.
The options object passed to the constructor of IntersectionObserver
specifies a threshold of zero. This means that as soon as any part of the observed element intersects with any part of the viewport, the callback function will be called.
Inside the callback function, we loop over each entry in the entries
array. If an entry's isIntersecting
property is true
, then we know that the observed element is now visible in the viewport.
Using the useEffect
hook, we can watch the intersection changes of the element.
React.useEffect(() => {
const element = elementRef.current;
if (!element) {
return;
}
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
// The element is visible ...
}
});
}, {
threshold: 0,
});
observer.observe(element);
return (): void => {
observer.unobserve(element);
};
}, []);
To get the URL of our background image, we first need to reference the element using entry.target
. Then, we retrieve the image URL from our custom data attribute data-background-src
.
Once we have the URL, we remove the custom data attribute by calling ele.removeAttribute('data-background-src')
. This step ensures that we don't accidentally reset the background image if the same code runs again, such as when an element is scrolled out of view and then back into view.
Finally, we update the background-image
style by setting it equal to our retrieved URL using string interpolation: ele.style.backgroundImage = `url(${src})`
. This will make sure our background image is displayed correctly.
// The element is visible
const ele = entry.target;
const src = ele.getAttribute('data-background-src');
ele.removeAttribute('data-background-src');
// Update the `background-image` style
ele.style.backgroundImage = `url(${src})`;
observer.unobserve(ele);
You can see it in action by checking out the live demo below.
Image credit: 10019, New York, United States by @benobro
Elevating user experience with a loading indicator
Let's take our user experience to the next level by adding a loading indicator that appears while the background image is loading. This lets users know that an image is on its way and helps prevent frustration and confusion.
We can achieve this by defining an enumeration with three possible values: NotLoaded
, Loading
, and Loaded
. The NotLoaded
value means the image hasn't loaded yet. The Loading
value indicates that the image is currently loading. Lastly, the Loaded
value indicates that the image has fully loaded.
enum Status {
NotLoaded,
Loading,
Loaded,
}
We're adding a new status
state to manage the loading status of the background image. Initially, the state is set to NotLoaded
.
const [status, setStatus] = React.useState(Status.NotLoaded);
Once the image becomes visible, the state changes to Loading
. Here's how the status changes with the modified code:
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const ele = entry.target;
setStatus(Status.Loading);
}
});
}, {
threshold: 0,
});
When the status
state changes to Loading
, we create a new Image
element and set its source to the URL of our background image. Then, we add an event listener for the load
event, which fires once the image finishes loading.
Inside this event listener, we update the status
state to Loaded
. This change triggers a re-render of our component, which also removes our custom data attribute and updates the background-image
style with our retrieved URL.
const image = new Image();
image.src = src;
image.addEventListener('load', () => {
setStatus(Status.Loaded);
ele.removeAttribute('data-background-src');
// Update the `background-image` style
ele.style.backgroundImage = `url(${src})`;
});
When we update the state, it triggers a re-render, allowing us to display different content based on whether or not the background image has finished loading. For example, we can show a loading message by checking if the status is set to Loading
.
<div className="container">
{status === Status.Loading && (
<div className="loading">Loading ...</div>
)}
</div>
To see how to position the loading indicator, take a look at the previous post. You can also check out the demo below and scroll down to the bottom to see the loading indicator in action until the image is fully loaded.
Conclusion
To sum up, lazy loading background images is a great way to boost your website's performance. With the IntersectionObserver API, you can detect when an element is visible on the screen and load its background image dynamically. Plus, by adding a loading indicator, you can improve the user experience while the image is loading.
If you want more helpful content like this, feel free to follow me:
Top comments (0)