DEV Community

Cover image for Lazy load an image
Phuoc Nguyen
Phuoc Nguyen

Posted on • Originally published at phuoc.ng

Lazy load an image

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.

<img src="..." loading="..." />
Enter fullscreen mode Exit fullscreen mode

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:

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

Take a look at the demo below:

Image credit: 10019, New York, United States by @benobro

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.

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

// Render
<img
    ref={imageRef}
    data-src="..."
/>
Enter fullscreen mode Exit fullscreen mode

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.

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

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.

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.

<img
    ref={imageRef}
    data-src="..."
    className="loading"
/>
Enter fullscreen mode Exit fullscreen mode

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

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

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.

 enum Status {
    NotLoaded,
    Loading,
    Loaded,
}
Enter fullscreen mode Exit fullscreen mode

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

const [status, setStatus] = React.useState(Status.NotLoaded);
Enter fullscreen mode Exit fullscreen mode

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

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

// Render
<img onLoad={handleLoadImage} />
Enter fullscreen mode Exit fullscreen mode

As soon as the image becomes visible, the state changes to Loading. Here's how the status changes with the modification 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 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.

<div className="container">
    {status === Status.Loading && (
        <div className="loading">Loading ...</div>
    )}
</div>
Enter fullscreen mode Exit fullscreen mode

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.

.container {
    position: relative;
}

.loading {
    position: absolute;
    left: 0;
    top: 0;
    height: 100%;
    width: 100%;
}
Enter fullscreen mode Exit fullscreen mode

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.

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:

Top comments (1)

Collapse
 
bhanufyi profile image
bhanu prasad

I have started my blogging with the same topic of using data-* attributes and intersection observer api to lazy load images.

Super cool to see how you broke down the topic into simple series of blogs, will follow this approach in my next blogs ✌️

dev.to/bhanuprasadcherukuvada/enha...