If you haven’t had the chance to watch it yet, Paul Lewis put together an awesome video series that demonstrates how to build a media player alongside some of the great features of Progressive Web Apps. There are a series of videos on YouTube that take place over a couple of days as he builds each new part of the site. Watching videos of developers showcasing their work is definitely one of my favourite ways of learning new things!
As he was demoing his code, I noticed that he created an awesome little helper library to lazy load images. I am always looking for ways to improve the performance of my websites, so this definitely sparked my interest.
What’s the big deal with lazy loading?
The idea behind lazy loading images is that you wait until a user scrolls further down the page and the image comes into view before making the network request for it. If your web page contains multiple images, but you only load each image as they are scrolled into view, you’ll end up saving bandwidth as well as ensuring that your web page loads quicker.
To give you an idea of this in action, let’s imagine the following page with three images on it.
If the user lands on the page and only views the first image, we don’t want to load the image of the pizza at the bottom of the page until the user scrolls down and the image is actually in view. If we lazy load the image instead, it means that the user only downloads exactly what they need, when they need it, making your web page a lot leaner.
For the more experienced developer, you might be familiar with lazy loading images, after all this concept has been around for a while. So what’s new?! There are many lazy loading libraries out there at the moment that do great job. I have even previously written about one on this blog (many years ago). The problem is, almost all of these libraries tap into the scroll event or use a periodic timer that checks the bounding on an element before determining if it is in view. The problem with this approach is that it forces the browser to re-layout the entire page and in certain conditions will introduce considerable jank to your website. We can do better!
Intersection Observer to the rescue!
This is where Intersection Observer comes in. Intersection Observer is built into most modern browsers and lets you know when an observed element enters or exits the browser’s viewport. This makes it ideal because it is able to deliver data asynchronously and won’t impact the main thread, making it an efficient means of giving you feedback.
In Paul’s example, he shows how to use Intersection Observer to lazy load images when they come into viewport. I’ve taken his initial code and and tweaked it slightly to make it easier for me to understand. In this article, I am going to run through the basics of Intersection Observer and show you how you can start lazy loading images in a super efficient way.
Getting StartedImagine a basic HTML page with three images similar to the one above. On the web page, you’ll have image elements that are similar to the code below:
// img class="js-lazy-image" data-src="burger.png"
You may notice that in the code above, the image file has no src attribute. This is because it is using a data attribute called data-src that points to the image source. We will use this to load the images when they come into viewport. You may also notice that the image element also has a class called “js-lazy-image” - we will be using this shortly in our JavaScript code in order to determine which elements we want to lazy load.
Next, we need to create the code that is going to lazy load the images on the page. Inside our JavaScript file that will be included on the page, we need to create a new Intersection Observer.
// Get all of the images that are marked up to lazy load
const images = document.querySelectorAll('.js-lazy-image');
const config = {
// If the image gets within 50px in the Y axis, start the download.
rootMargin: '50px 0px',
threshold: 0.01
};
// The observer for the images on the page
let observer = new IntersectionObserver(onIntersection, config);
images.forEach(image => {
observer.observe(image);
});
The example above looks like a lot of code, but let’s break it down step by step. Firstly, I am selecting all of the images on the page that have the class “js-lazy-image”. Next, I’m creating a new IntersectionObserver and using it to observe all of the images that we have selected that have the class “js-lazy-image”. Using the default options for IntersectionObserver, your callback will be called both when the element comes partially into view and when it completely leaves the viewport. In this case, I am passing through a few extra configuration options to the IntersectionObserver. Using a rootMargin allows you to specify the margins for the root, effectively allowing you to either grow or shrink the area used for intersections. We want to ensure that if the image gets within 50px in the Y axis, we will start the download.
Now that we’ve created an Intersection Observer and are observing the images on the page, we can tap into the intersection event which will be fired when the element comes into view.
function onIntersection(entries) {
// Loop through the entries
entries.forEach(entry => {
// Are we in viewport?
if (entry.intersectionRatio > 0) {
// Stop watching and load the image
observer.unobserve(entry.target);
preloadImage(entry.target);
}
});
}
In the code above, whenever an element that we are observing comes into the users viewport, the onIntersection function will be triggered. At this point we can then loop through the images that we are observing and determine which one is in viewport. If the current element is in the intersection ratio, we know that the image is in the users viewport and we can load it. Once the image is loaded, we don’t need to observe it any more, and using unobserve() will remove it from the list of entries in the Intersection Observer.
That’s it! As soon as a user scrolls and the image comes into view, the appropriate image will be loaded. The best thing about this code is that Intersection Observer keeps it smoother than Barry White. I’ve tried to keep the code above as succinct as possible, but if you’d like to see the full version, I’ve created a Github repo with a working example of this in action.
Browser SupportAt this point, you might be wondering about browser support for this feature. Intersection Observer is currently supported in Edge, Firefox, Chrome, and Opera which is great news.
However, in order to ensure that our code doesn’t break for browsers that don’t support this feature, we can use feature detection to determine how we want to load the images. Let’s have a look at the code below.
// If we don't have support for intersection observer, load the images immediately
if (!('IntersectionObserver' in window)) {
Array.from(images).forEach(image => preloadImage(image));
} else {
// It is supported, load the images
observer = new IntersectionObserver(onIntersection, config);
images.forEach(image => {
observer.observe(image);
});
}
In the code above, we are checking to see if IntersectionObserver is available in the current browser and if it isn’t we simply load the images immediately, otherwise we use our default behaviour.
If you really like the ease of the Intersection Observer API and would like to use a polyfill instead, the WICG have created one that is available on their Github repository. The only downside of this is that you won’t get the performance benefits that the native implementation would give you.
You could even take this a step further and add support for users that don’t have JavaScript enabled as suggested by Robin Osborne.
SummaryIn this article, we’ve used Intersection Observer to lazy load images, but you can use it to do so much more. It could be used to determine if someone is looking at an advert or even if an element in an iFrame is in view. The easy to understand API makes it open to many options.
If you’d like to learn more about Intersection Observer, I recommend reading this informative article on the Google Developers site. I also highly recommend watching Paul Lewis’ video series on Youtube, it’s packed with great tips and you’ll definitely learn something.