Lazy load a background image
Tags: webdev, javascript, react, typescript
Published: 2024-04-03 01:24:13

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.

```tsx
<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.

```tsx
<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.

```tsx
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.

```tsx
// 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](https://unsplash.com/photos/wpU4veNGnHg) by [@benobro](https://unsplash.com/@benobro)

{% codesandbox xd4cfz %}

## 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.

```tsx
 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`.

```tsx
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:

```tsx
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.

```tsx
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`.

```tsx
<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.

{% codesandbox tmt86d %}

## 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:

-   [DEV](https://dev.to/phuocng)
-   [GitHub](https://github.com/phuocng)
-   [Website](https://phuoc.ng)