DEV Community

Cover image for Lazy load a background image
Phuoc Nguyen
Phuoc Nguyen

Posted on • Originally published at phuoc.ng

Lazy load a background image

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(...)`
    }}
/>
Enter fullscreen mode Exit fullscreen mode

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://...`}
/>
Enter fullscreen mode Exit fullscreen mode

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);
    };
}, []);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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,
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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,
});
Enter fullscreen mode Exit fullscreen mode

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})`;
});
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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)