Lazy loading images

Images can appear on a webpage due to being inline in the HTML as <img> elements or as CSS background images. In this post you will find out how to lazy-load both types of image.

Inline images

The most common lazy loading candidates are images as used in <img> elements. With inline images we have three options for lazy loading, which may be used in combination for the best compatibility across browsers:

Using browser-level lazy loading

Chrome and Firefox both support lazy loading with the loading attribute. This attribute can be added to <img> elements, and also to <iframe> elements. A value of lazy tells the browser to load the image immediately if it is in the viewport, and to fetch other images when the user scrolls near them.

See the loading field of MDN's browser compatibility table for details of browser support. If the browser does not support lazy loading then the attribute will be ignored and images will load immediately, as normal.

For most websites, adding this attribute to inline images will be a performance boost and save users loading images that they may not ever scroll to. If you have large numbers of images and want to be sure that users of browsers without support for lazy loading benefit you will need to combine this with one of the methods explained next.

To learn more, check out Browser-level lazy loading for the web.

Using Intersection Observer

To polyfill lazy loading of <img> elements, we use JavaScript to check if they're in the viewport. If they are, their src (and sometimes srcset) attributes are populated with URLs to the desired image content.

If you've written lazy loading code before, you may have accomplished your task by using event handlers such as scroll or resize. While this approach is the most compatible across browsers, modern browsers offer a more performant and efficient way to do the work of checking element visibility via the Intersection Observer API.

Intersection Observer is easier to use and read than code relying on various event handlers, because you only need to register an observer to watch elements rather than writing tedious element visibility detection code. All that's left to do is to decide what to do when an element is visible. Let's assume this basic markup pattern for your lazily loaded <img> elements:

<img class="lazy" src="placeholder-image.jpg" data-src="image-to-lazy-load-1x.jpg" data-srcset="image-to-lazy-load-2x.jpg 2x, image-to-lazy-load-1x.jpg 1x" alt="I'm an image!">

There are three relevant pieces of this markup that you should focus on:

  1. The class attribute, which is what you'll select the element with in JavaScript.
  2. The src attribute, which references a placeholder image that will appear when the page first loads.
  3. The data-src and data-srcset attributes, which are placeholder attributes containing the URL for the image you'll load once the element is in the viewport.

Now let's see how to use Intersection Observer in JavaScript to lazy-load images using this markup pattern:

document.addEventListener("DOMContentLoaded", function() {
  var lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));

  if ("IntersectionObserver" in window) {
    let lazyImageObserver = new IntersectionObserver(function(entries, observer) {
      entries.forEach(function(entry) {
        if (entry.isIntersecting) {
          let lazyImage = entry.target;
          lazyImage.src = lazyImage.dataset.src;
          lazyImage.srcset = lazyImage.dataset.srcset;
          lazyImage.classList.remove("lazy");
          lazyImageObserver.unobserve(lazyImage);
        }
      });
    });

    lazyImages.forEach(function(lazyImage) {
      lazyImageObserver.observe(lazyImage);
    });
  } else {
    // Possibly fall back to event handlers here
  }
});

On the document's DOMContentLoaded event, this script queries the DOM for all <img> elements with a class of lazy. If Intersection Observer is available, create a new observer that runs a callback when img.lazy elements enter the viewport.

Intersection Observer is available in all modern browsers. Therefore using it as a polyfill for loading="lazy" will ensure that lazy loading is available for most visitors.

Images in CSS

While <img> tags are the most common way of using images on web pages, images can also be invoked via the CSS background-image property (and other properties). Browser-level lazy loading does not apply to CSS background images, so you need to consider other methods if you have background images to lazy-load.

Unlike <img> elements which load regardless of their visibility, image loading behavior in CSS is done with more speculation. When the document and CSS object models and render tree are built, the browser examines how CSS is applied to a document before requesting external resources. If the browser has determined a CSS rule involving an external resource doesn't apply to the document as it's currently constructed, the browser doesn't request it.

This speculative behavior can be used to defer the loading of images in CSS by using JavaScript to determine when an element is within the viewport, and subsequently applying a class to that element that applies styling invoking a background image. This causes the image to be downloaded at the time of need instead of at initial load. For example, let's take an element that contains a large hero background image:

<div class="lazy-background">
  <h1>Here's a hero heading to get your attention!</h1>
  <p>Here's hero copy to convince you to buy a thing!</p>
  <a href="/buy-a-thing">Buy a thing!</a>
</div>

The div.lazy-background element would normally contain the hero background image invoked by some CSS. In this lazy loading example, however, you can isolate the div.lazy-background element's background-image property via a visible class added to the element when it's in the viewport:

.lazy-background {
  background-image: url("hero-placeholder.jpg"); /* Placeholder image */
}

.lazy-background.visible {
  background-image: url("hero.jpg"); /* The final image */
}

From here, use JavaScript to check if the element is in the viewport (with Intersection Observer!), and add the visible class to the div.lazy-background element at that time, which loads the image:

document.addEventListener("DOMContentLoaded", function() {
  var lazyBackgrounds = [].slice.call(document.querySelectorAll(".lazy-background"));

  if ("IntersectionObserver" in window) {
    let lazyBackgroundObserver = new IntersectionObserver(function(entries, observer) {
      entries.forEach(function(entry) {
        if (entry.isIntersecting) {
          entry.target.classList.add("visible");
          lazyBackgroundObserver.unobserve(entry.target);
        }
      });
    });

    lazyBackgrounds.forEach(function(lazyBackground) {
      lazyBackgroundObserver.observe(lazyBackground);
    });
  }
});

Effects on Largest Contentful Paint (LCP)

Lazy loading is a great optimization that reduces both overall data usage and network contention during startup by deferring the loading of images to when they're actually needed. This can improve startup time, and reduce processing on the main thread by reducing time needed for image decodes.

However, lazy loading is a technique that can affect your website's Largest Contentful Paint LCP in a negative way if you're too eager with the technique. One thing you will want to avoid is lazy loading images that are in the viewport during startup.

When using JavaScript-based lazy loaders, you will want to avoid lazy loading in-viewport images because these solutions often use a data-src or data-srcset attribute as a placeholder for src and srcset attributes. The problem here is that the loading of these images will be delayed because the browser preload scanner can't find them during startup.

Even using browser-level lazy loading to lazy load an in-viewport image can backfire. When loading="lazy" is applied to an in-viewport image, that image will be delayed until the browser knows for sure it's in the viewport, which can affect a page's LCP.

Never lazy load images that are visible in the viewport during startup. It's a pattern that will affect your site's LCP negatively, and therefore the user experience. If you need an image at startup, load it at startup as quickly as possible by not lazy loading it!

Lazy loading libraries

You should use browser-level lazy loading whenever possible, but if you find yourself in a situation where that isn't an option—such as a significant group of users still reliant on older browsers—the following libraries can be used to lazy-load images:

  • lazysizes is a full-featured lazy loading library that lazy-loads images and iframes. The pattern it uses is quite similar to the code examples shown here in that it automatically binds to a lazyload class on <img> elements, and requires you to specify image URLs in data-src and/or data-srcset attributes, the contents of which are swapped into src and/or srcset attributes, respectively. It uses Intersection Observer (which you can polyfill), and can be extended with a number of plugins to do things like lazy-load video. Find out more about using lazysizes.
  • vanilla-lazyload is a lightweight option for lazy loading images, background images, videos, iframes, and scripts. It leverages Intersection Observer, supports responsive images, and enables browser-level lazy loading.
  • lozad.js is a another lightweight option that uses Intersection Observer only. As such, it's highly performant, but will need to be polyfilled before you can use it on older browsers.
  • If you need a React-specific lazy loading library, consider react-lazyload. While it doesn't use Intersection Observer, it does provide a familiar method of lazy loading images for those accustomed to developing applications with React.