const - JS | lectureAdvanced DOM and Events

Lazy Loading Images

Advanced DOM and Events

One of the most important things when building any website is performance. And images have by far the biggest impact on page load time. Hence, it's very important to optimize images on any page. And for that, we can use a strategy called lazy loading images. So, let's see how to implement this strategy using JavaScript.

Below is a video of the final result of this lecture:

From the above video, you can see that as we approach the different images, they start to load, and once it's finished, the image gets displayed. And the placeholder image is replaced by the actual image. And this is what we're going to build in this lecture.

The main ingredient to this lazy loading strategy is that we have a low-resolution image loaded first. From the HTML, we can see that the dimensions of all three images are 227x151. And if you download the image of the first section, for example, you will see that it makes 17KB while the real image, which is the link set as the value of the data-src attribute, is around 3MB. So that's a huge difference.

index.html
<img
  src="https://source.unsplash.com/00WMDrZX3YY/227x151"
  data-src="https://source.unsplash.com/00WMDrZX3YY/6394x4262"
  alt="Genie"
  class="section__info__img lazy-img"
/>

So the idea is that when we scroll to one of those images, we will then replace the low-resolution image with the one specified in the data-src attribute. And we are also going to remove the class lazy-img from the image, which is the class that makes the image look blurry. Because without this filter, the image quality is not that great.

style.css
.lazy-img {
  filter: blur(20px);
}

Enough talking; let's get started 😉. And as always, before we start, head over to this link and download the starter files for this lecture.

To implement this strategy, we will once more use the IntersectionObserver API. And note that lazy loading is really great for performance. While the other things we did so far are more visual, this one really impacts how your site works, especially for your users who might have a slow internet connection, a low data plan, or a slow device. Hence, we always have to think about these users as well.

Let's start by selecting our images.

script.js
const imageTargets = document.querySelectorAll('img[data-src]');
console.log(imageTargets);

Alright I got you, why not just do document.querySelectorAll('img')? Well, In this case, we could have done that, and that won't be a problem because we know that all the images need to be lazy loaded. But in a real-world scenario, you might have some images you don't want to lazy load, which is the case I'm trying to show you here. So with this selector, we are only selecting the images with the data-src attribute because that's where we specified the high-resolution image.

Lazy Loading Images

Next, let's create our observer, callback function, and options object.

script.js
const lazyLoadImages = (entries, observer) => {
  const [entry] = entries;
  console.log(entry);
};

const imagesObserverOptions = {
  root: null,
  // You can set it to 0.4 if you want the same effect as in the video
  threshold: 0,};

const imagesObserver = new IntersectionObserver(
  lazyLoadImages,
  imagesObserverOptions
);

Now, let's attach our image observer to our image targets.

script.js
imageTargets.forEach((imageEl) => imagesObserver.observe(imageEL));

Lazy Loading Images

Let's now add some logic to our callback function to only do something if our image is intersecting.

script.js
const lazyLoadImages = (entries, observer) => {
  const [entry] = entries;

  // Guard clause
  if (!entry.isIntersecting) return;

  // Replace src attribute with data-src
  entry.target.src = entry.target.dataset.src;};

The next thing we need to do is to remove the class (lazy-img) ) that applies the blur filter to the image. How do we do that? Well, it is a little bit tricky because replacing the source (src) attribute of an image actually happens behind the scenes. That is, JavaScript loads and displays the new image behind the scenes. And once JavaScript finishes loading the new image, it will trigger the load event. And the load event is just like any other event. Hence, we can listen to it and then do whatever we want.

script.js
const lazyLoadImages = (entries, observer) => {
  const [entry] = entries;

  // Guard clause
  if (!entry.isIntersecting) return;

  // Replace src attribute with data-src
  entry.target.src = entry.target.dataset.src;

  // Listen to load event and remove lazy-img class
  entry.target.addEventListener('load', (e) => {    e.target.classList.remove('lazy-img');  });};

If you instead remove the class lazy-img out of the addEventListener method, so something like this:

script.js
const lazyLoadImages = (entries, observer) => {
  const [entry] = entries;

  // Guard clause
  if (!entry.isIntersecting) return;

  // Replace src attribute with data-src
  entry.target.src = entry.target.dataset.src;

  entry.target.classList.remove('lazy-img');};

You will notice that the class is removed before the image is loaded. In normal conditions, you might not notice this. So if you want to try this out, go to the network tab in the dev tools and set the network speed to Slow 3G. And then, you will see that the class is removed before the image is loaded.

Lazy Loading Images

The last thing we need to do is to unobserve the image once it has been loaded.

script.js
const lazyLoadImages = (entries, observer) => {
  const [entry] = entries;

  // Guard clause
  if (!entry.isIntersecting) return;

  // Replace src attribute with data-src
  entry.target.src = entry.target.dataset.src;

  // Listen to load event and remove lazy-img class
  entry.target.addEventListener('load', (e) => {
    e.target.classList.remove('lazy-img');
  });

  observer.unobserve(e.target);};

Great! So, to finish, I hope that with this lecture, you can now see how important it is to implement this kind of functionality into your projects.