Lazy-loading images with the Intersection Observer

Lazy-loading images with the Intersection Observer

Lazy loading has always been a popular toppic on the web! But with the Intersection Observer that will all be so much better than it was!

Last year I tried to focus more and more on the performance of websites and web-apps. It’s quite a challenge in the world of all the big frameworks and libraries.

Hopefully, you know that loading images can cost even more time and data. But lucky that we are, there is a technique called lazy-loading images.

Loading images

Loading images with the normal <img> tag, let’s the browser wait for being ready until all of the images are loaded.

  <img src="https://loadyourimages.nl/image-nam.jpg" src="This is my awesome image">

Especially if you have a website with a lot of images, it can take maybe up till 10 seconds before the user has an interactive page.

On mobile connections that can even worse. With bad or slow connections your user sometimes will wait for tens of seconds or even minutes.

But we all know that those users want to wait that long! They will leave after a few seconds!

Lazy-loading images

We want our pages loaded as fast as it is possible. Our goal should be 0 - 5 seconds, this is the amount of time a user will be patient for the page to load.

So if we avoid the normal <img src="URL">, so our pages will load a lot faster.

If we use a data-attribute to put in the URL of the image, we can put it in the src attribute to load them when it's in the viewport.

Most developers will use libraries for lazy-loading that are using an eventListener on the scroll event to check if an element is in the viewport. But we need something better since the eventListener on the scroll is kind of heavy on some devices!

Intersection Observer

The Intersection Observer does a pretty good job in detecting if a certain element is inside the visible part of your browser.

The Intersection Observer API provides a way to asynchronously observe changes in the intersection of a target element with an ancestor element or with a top-level document's viewport. source: Mozilla Docs

With this API we can listen to an element is in the viewport if they intersecting with the viewport.

Configure the Intersection Observer

We have a few options in configuring the observer.

const options = {
  root: document.querySelector('#container'),
  rootMargin: '0px',
  threshold: 1.0,
}

const observer = new IntersectionObserver(callback, options)

root

In the root property define the element that will be set as the viewport. Keep in mind that if you target an element (or the body) and when its height is on auto than all the elements will be set to visible. So if you set an element, set a height that is not auto, otherwise, it won't work like expected. If you don't define this property it will automatically use the browser viewport.

rootMargin

If the rootMargin value is set to 0, it will not look outside the root element. If you put in 10px, it will check earlier if an element is scrolled into it's root element.

threshold

If this value of threshold is 0, it will run the callback when 1px of the element is inside the root element. When the value is 1.0 it will trigger the callback when it's 100% inside the root element. But if you want the callback to be called when the element is 50% inside the root element, put in the value 0.5.

Target elements

In order to use the Intersection Observer, we need an element to observe some elements!

The elements we want to observe have a class name 'fake-image', and we are gonna loop through the elements to start an observer for every fake-image element.

We also want to make sure, that when the element is in our root element, that the observation will be stopped. This saves some power on your computer or device.

const io = new IntersectionObserver(entries => {
  entries.forEach(entry => {
    if (entry.intersectionRatio > 0) {
      io.unobserve(entry.target)
    }
  })
}, options)

const targetElements = document.querySelector('.fake-image')
for (let element of targetElements) {
  io.observe(element)
}

Demo

Let's try this out!

I've created a small demo to show you that Intersection Observer works really smooth.

Start scrolling, everytime an element is 100% visible it will turn green and the text will be "Loaded!".

See the Pen Example Intersection Observer by Raymon Schouwenaar (@rsschouwenaar) on CodePen.

Browser support

The support for the intersection observer is pretty nice. It's implemented in Chrome (desktop & mobile), Firefox, Edge, Android browser. So it's missing in IE11 and Safari (desktop & mobile). The webkit team is working hard on it because it's marked as "In Development" 👍, so hopefully it will soon be supported in Safari (March 2018).

To support the browsers that don't support this cool API (yet) we can use a polyfill. We can get it via npm: intersection-observer npm install intersection-observer --save.

Let's build lazy loaded images

Now we know how we can use the intersection observer, we're gonna make our images load asynchronously into our browser when they are in the visible viewport.

At the beginning of this blogpost, I've shown you how most of the lazy load functionality is build last years. So let's replace the scroll event listener for the intersection observer.

HTML

If you remember the HTML we used in the example before then you see we only need to add a <img> tag with the data-attribute data-src.

A data-attribute is a perfect solution to put in the URL, so we can put the URL in theirs.

    <div class="fake-image">
      <h2>Fake image</h2>
        <img data-src="https://images.unsplash.com/photo-1521315807607-8220d719f0e4?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjF9&s=558cae1f662f20ea31f3ffda21a766f8" alt="" />
    </div>

JavaScript

For the JavaScript, we only need one function to make our image load. Call the function inside the intersection observer.

const io = new IntersectionObserver(entries => {
  entries.forEach(entry => {
    // console.log('entry: ', entry);
    if (entry.intersectionRatio > 0.9) {
      loadImage(entry.target)
      entry.target.classList.add('active')
      // entry.target.innerHTML = '<h2>Loaded!</h2>';
      io.unobserve(entry.target)
    }
  })
}, options)

function loadImage(imageElement) {
  setTimeout(() => {
    console.dir(imageElement.querySelector('img'))
    imageElement.querySelector('img').src = imageElement.querySelector(
      'img',
    ).dataset.src
  }, 1000)
}

The only thing we need to do in the function is, putting the URL from the data-src attribute into the src attribute.

When the src attribute will be present, the image will load in the browser.

In the JavaScript code, I've put in a timeout for 1 sec to see the loading happen.

Example

Let's check out the example. Scroll the view with images down so you can see for yourself that it works.

See the Pen Lazy Load image w/ Intersection Observer 🚀 by Raymon Schouwenaar (@rsschouwenaar) on CodePen.

Thanks

If you learned something or have other ways to loop over a NodeList Object from the querySelectorAll, please let me know on twitter 😉: @rsschouwenaar

Did you find this article valuable?

Support Dev By RayRay by becoming a sponsor. Any amount is appreciated!