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="..." />
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
}
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="..."
/>
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);
};
}, []);
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"
/>
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;
}
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,
}
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);
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} />
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,
});
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>
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%;
}
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)
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...