Lazy load an image
Tags: webdev, javascript, typescript, react
Published: 2024-04-02 01:36:50

Lazy loading images is a smart way to improve website performance. It's a technique that defers the loading of non-critical images until they're needed. By doing this, websites can reduce the initial load time of a webpage, which can have a big impact on user experience and engagement.

Lazy loading only loads images when they're necessary, so websites can minimize the amount of data that needs to be downloaded and processed. This results in faster page load times and better overall performance. Plus, lazy loading can help reduce server load and bandwidth usage, which is especially useful for sites with high traffic volumes or limited resources.

In this post, we'll show you how to lazy load an image using IntersectionObserver.

## Introducing the loading attribute

Did you know that modern browsers support lazy loading of images with the `loading` attribute? This attribute is a relatively new addition to the HTML standard that makes it easy for developers to implement lazy loading without relying on third-party libraries or custom code.

When the `loading` attribute is added to an `<img>` tag, it tells the browser to defer loading the image until it's either within the viewport or has been scrolled into view. This can be especially useful for images that are placed below the fold or out of view, as they won't be loaded until they're actually needed.

```html
<img src="..." loading="..." />
```

The `loading` attribute has two possible values: `lazy` and `eager`. By default, it's set to `eager`, which means the image will be loaded immediately, potentially slowing down the page. However, if you set it to `lazy`, the image will only be loaded when it enters the viewport. It's important to note that not all browsers support both values of the `loading` attribute, so it's a good idea to check for compatibility before using it in production.

To check if the `loading` attribute is supported in a browser, you can use JavaScript to see if `loading` is a property of `HTMLImageElement.prototype`. If it is, you can use the lazy loading technique. Here's an example:

```js
if ('loading' in HTMLImageElement.prototype) {
    // `loading` attribute is supported
} else {
    // `loading` attribute is not supported
    // Use IntersectionObserver or other techniques for lazy loading
}
```

Take a look at the demo below:

> Image credit: [10019, New York, United States](https://unsplash.com/photos/wpU4veNGnHg) by [@benobro](https://unsplash.com/@benobro)

{% codesandbox npnd9v %}

## The limitations of the loading attribute

While the `loading` attribute is great for implementing lazy loading on your website, it does have some limitations you should be aware of. One major limitation is that it only works for images, not other assets like videos or iframes. This means you'll need to use a different technique or library if you want to lazy load these assets.

Another limitation is browser support. While most modern browsers support the `loading` attribute, some older browsers may not recognize it and will simply ignore it. This can affect page performance if the image is loaded as usual.

Lastly, we can't customize the UI when an image is being loaded using the `loading` attribute. In the next sections, we'll explore how to use IntersectionObserver to replace the `loading` attribute.

## Only loading images when visible

To prevent browsers from loading images automatically, we'll replace the `src` attribute with a custom data attribute named `data-src`. Its value is the same as the original `src` attribute.

Once the image is visible, we'll replace the `data-src` attribute with the `src` attribute, asking the browser to load the image as usual.

To achieve this, we'll use a React ref to represent the image element. The reference is then attached to the image via the `ref` attribute.

```tsx
const imageRef = React.useRef<HTMLImageElement>(null);

// Render
<img
    ref={imageRef}
    data-src="..."
/>
```

Next, we'll use `IntersectionObserver` to keep an eye on the image and detect when it enters or exits the viewport.

In the following code, we're creating a new instance of `IntersectionObserver`. This observer calls a function when an observed element intersects with the root element or its own bounding box.

```tsx
React.useEffect(() => {
    const image = imageRef.current;
    if (!image) {
        return;
    }

    const observer = new IntersectionObserver((entries) => {
        entries.forEach((entry) => {
            if (entry.isIntersecting) {
                // The image is visible
                const ele = entry.target;

                observer.unobserve(ele);
                ele.setAttribute('src', ele.getAttribute('data-src'));
                ele.removeAttribute('data-src');
            }
        });
    }, {
        threshold: 0,
    });
    observer.observe(image);

    return (): void => {
        observer.unobserve(image);
    };
}, []);
```

When an image becomes visible, we swap out the `data-src` attribute with the `src` attribute, which tells the browser to load the image as usual. We make sure to avoid loading images multiple times by unobserving the element once it's loaded.

The `threshold` option sets how much of an element needs to be visible before it's considered "intersecting". We set it to 0 so that even a single pixel of the image being visible will trigger its loading.

Lastly, to prevent memory leaks and improve performance, we detach the observer from the image element using its `unobserve()` method inside a cleanup function returned by the `useEffect` hook.

You can check out the live demo below to see it in action.

{% codesandbox mppfgk %}

## Enhancing user experience with a loading indicator

To take our user experience to the next level, let's talk about adding a loading indicator that shows up while an image is loading. This lets users know that an image is on its way and helps to prevent frustration or confusion.

To add a loading indicator, we can use CSS to create a spinner animation and then apply it to the image element. The first step is to add a new CSS class to our image element called `loading`.

```tsx
<img
    ref={imageRef}
    data-src="..."
    className="loading"
/>
```

Now, let's define the `loading` class in our CSS definitions.

```css
.loading {
    display: block;
    width: 100%;
    height: auto;
    background-image: url('/path/to/spinner.svg');
    background-repeat: no-repeat;
    background-position: center center;
}

.loading:not([src])::after {
    content: "";
    display: block;
    width: 100%;
    height: auto;
}
```

In this example, we've added a spinner SVG as the background image for our loading class. We've set the `background-repeat` property to `no-repeat` so that it only displays once and centered it inside the container using `background-position`.

To make sure the loading indicator always displays, we've added a pseudo-element `::after` with `content` set to an empty string. This is useful when there isn't an existing `src` attribute (i.e., when `data-src` is still being used).

With this implementation, a spinner will appear while the image is loading. Once the image is fully loaded, the spinner disappears and the actual image is displayed.

On the other hand, using another external SVG could potentially slow down the page's performance as browsers need to load the file. Additionally, customizing the loading indicator isn't an easy task.

To tackle these issues, we can define an enum with three possible values: `NotLoaded`, `Loading`, and `Loaded`. The `NotLoaded` value means that the image hasn't been loaded yet. The `Loading` value indicates that the image is currently being loaded. Lastly, the `Loaded` value indicates that the image has been fully loaded.

```tsx
 enum Status {
    NotLoaded,
    Loading,
    Loaded,
}
```

We add a new `status` state to manage the loading status of an image. At first, the state is set to `NotLoaded`.

```tsx
const [status, setStatus] = React.useState(Status.NotLoaded);
```

Once the browser finishes loading the image, a function called `handleLoadImage()` updates the `status` state to `Loaded`.

```tsx
const handleLoadImage = () => {
    setStatus(Status.Loaded);
};

// Render
<img onLoad={handleLoadImage} />
```

As soon as the image becomes visible, the state changes to `Loading`. Here's how the status changes with the modification code:

```tsx
const observer = new IntersectionObserver((entries) => {
    entries.forEach((entry) => {
        if (entry.isIntersecting) {
            const ele = entry.target;
            setStatus(Status.Loading);
        }
    });
}, {
    threshold: 0,
});
```

When we update the state, it triggers a re-render, which lets us display different content based on whether or not an image has finished loading. For instance, 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 position the loading indicator, we use CSS to set its position to absolute within a container. This allows the loading indicator to be overlaid on top of the image while it's being loaded.

To get started, make sure that the parent container has a `position: relative` property set. This allows child elements with absolute positioning to be positioned relative to the parent container. Next, define the `.loading` class with an absolute position and set its `left`, `top`, `width`, and `height` properties to cover the entire parent container.

```css
.container {
    position: relative;
}

.loading {
    position: absolute;
    left: 0;
    top: 0;
    height: 100%;
    width: 100%;
}
```

This implementation positions a loading indicator over an image's parent container while the image loads. Once the image is fully loaded, the indicator disappears and the actual image is displayed.

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 mxwcq9 %}

## Conclusion

In conclusion, lazy loading images is an excellent way to boost your website's performance by decreasing the amount of data that needs to be loaded upfront. We can use IntersectionObserver to detect when an image is visible and only load it at that point, instead of loading all images when the page first loads.

Furthermore, we can add a loading indicator to give users feedback that an image is on its way and prevent frustration or confusion. By conditionally rendering different content based on whether or not the image has finished loading, we can provide a better user experience.

Overall, implementing lazy loading images with a loading indicator can significantly enhance the performance and user experience of your website.

---

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)