Lazy Loading

Do not miss this exclusive book on Binary Tree Problems. Get it now for free.

In this article, we have explored the idea of Lazy Loading in depth. It is an optimization technique to help webpages load faster.

Table of contents:

  1. Introduction
  2. Performance
  3. Code Splitting
  4. CSS Optimization
  5. Lazy-load images
  6. Lazy-load video

Introduction

Lazy loading is a method of identifying non-blocking/non-critical web resources and loading them when necessary. This can improve the performance of web pages by reducing rendering times and cache sizes.

Performance

Directly improving the performance of webpages is vital to improving user experiences, conversions, user retention, and scalability.

We often want to measure performance using RAIL:

  • Response: time for completion of common processes after user actions.
  • Animation: time for UI animations in response to user actions.
  • Idle: time for deffered work during moments of no user interaction.
  • Load: time to deliver interactive content.

Most companies will place Performance budgets laying out time constraints for the above metrics. Typically we want to reduce response, animation, and load times while maximizing idle times.

Lazy Loading is a method of improving load times by using available idle time to load non-blocking resources.

Lazy-loading usually occurs during user interactions such as scrolling and navigation.

Code Splitting

JavaScript, CSS and HTML can be split into smaller chunks. This enables us to send smaller packets of code improving page-load times. The rest of the application or webpage can be loaded on demand.

Two methods of code splitting are:

  • Entry point splitting: separates code by entry point(s) in the app
  • Dynamic splitting: separates code where dynamic import() statements are used in
    JavaScript

We can also import javascript as ES6 modules, which have optimizations like default deferral. When loading javascript into html, the script tag with type="module" is treated as a JavaScript module.

<script src="filename.js" type="module"></script>

CSS Optimizations

CSS is a render blocking resource that, and is usually imported along with other stylesheets for purposes such as styling for print, different orientations, screen-sizes, and ui elements. By specifying a media type, the browser will decide which stylesheet to load depending where the CSS needs to be applied.

<link rel="stylesheet" href="styles.css"> <!-- blocking -->
<link rel="stylesheet" href="print.css" media="print"> <!-- not blocking -->
<link rel="stylesheet" href="mobile.css" media="screen and (max-width: 480px)"> <!-- not blocking on large screens -->

Fonts

By default text rendering is defered for fonts. We can preload fonts using the following options:

  • <link rel="preload">
  • the CSS font-display property
  • Font Loading API.

Lazy-Loading Images

Images typically contribute the most to data-usage and page load times. At most a few images are present in the viewport at a time, so we can use the following methods to detect when images enter the viewport to begin loading them.

The three primary methods are:

  • Browser-level lazy-loading
  • Intersection Observer
  • Scroll, Resize, and OrientationChange event handlers

Browser-Level Lazy-Loading

The loading attribute on an <img>/<iframe> element can be used to instruct browsers to defer loading of images/iframes that are off-screen until the user scrolls near them.

<!-- for images -->
<img src="image.jpg" alt="..." loading="lazy">

<!-- for iframes -->
<iframe src="video-player.html" title="..." loading="lazy"></iframe>

The load event fires when the eagerly-loaded content has all been loaded; at that time, it's entirely possible (or even likely) that there may be lazily-loaded images that are within the visual viewport that haven't yet loaded.

You can determine if a given image has finished loading by examining the value of its Boolean complete property.

let doneLoading = htmlImageElement.complete;

Intersection Observer

The Intersection Observer API allows users to know when an observed element enters or exits the browser’s viewport. This isn't available on older browsers, but offers a more efficient alternative to using event handlers in modern browsers.

An Intersection Observer can examine multiple elements at once and manage observing state changes, unlike event handlers which require you to specify what a state change is.

Assuming we have the following markup:

<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"
/>

There are four pieces of markup to focus on:

  • class: the way we identify the img in JavaScript.
  • src: references a placeholder image that will appear when the page first loads.
  • data-src and data-srcset: these are placeholder attributes containing the URL for the image you'll load once the element is in the viewport. srcset is used for responsive images, where the browser will load images at different resolutions automatically.

Normally the placeholder image is at a lower resolution so it can be loaded faster.

We can then define our Intersection Oberserver in the following JavaScript:

// When DOM Content is loaded do the following
document.addEventListener("DOMContentLoaded", function() {

  // get all images with the class 'lazy'
  var lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));

  // if the browser supports the IntersectionObserver API
  if ("IntersectionObserver" in window) {
  
    // Create a new IntersectionObserver
    let lazyImageObserver = new IntersectionObserver(function(entries, observer) {
    
      entries.forEach(function(entry) {
      
        // for each image of class lazy in the viewport
        if (entry.isIntersecting) {
           
          // get the current image HTML element
          let lazyImage = entry.target;
          
          // load the correct image and other responsive image 
          // from the data attribute
          lazyImage.src = lazyImage.dataset.src;
          lazyImage.srcset = lazyImage.dataset.srcset;
          
          // remove the lazy class from the image
          lazyImage.classList.remove("lazy");
          
          // remove the image from the observer (since it has been loaded)
          lazyImageObserver.unobserve(lazyImage);
        }
      });
    });

    // for each 'lazy' image
    lazyImages.forEach(function(lazyImage) {
    
      // apply the observer function to the image (loads when image enters viewport)
      lazyImageObserver.observe(lazyImage);
    
    });
  } else {
    // Possibly fall back to event handlers here
  }
});

The code above gives an overview of how to define an IntersectionObserver in comments identified by //. In summary of the process:

  • We load an array of lazy images
  • Apply an observer to them
  • Have the observer detect when an image is in the viewport
  • Load the correct images, replacing the placeholder
  • Remove the lazy image from the array of lazy ones and the observer from the loaded image

Event Handlers

Using the same HTML in the previous section, loading a set of images with the following attributes:

<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"
/>

We can create our own Observer using event handlers. This will also be backwards compatible and work on browsers that don't support the Intersection Observer API

// adds an event listener to run once DOM loads
document.addEventListener("DOMContentLoaded", function() {

  // get all images with class 'lazy'
  let lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));
  let active = false;

  const lazyLoad = function() {
    if (active === false) {
      active = true;
        
      // wait 200 ms before we begin loading lazy images in the viewport 
      // used to detect if a user is viewing the image or scrolling past it
      setTimeout(function() {
      
        // for each lazy image
        lazyImages.forEach(function(lazyImage) {
        
          //  check if image is in viewport
          let in_viewport = (
             lazyImage.getBoundingClientRect().top <= window.innerHeight &&
             lazyImage.getBoundingClientRect().bottom >= 0
          )
          
          // check if image is not visible
          let is_not_visible = getComputedStyle(lazyImage).display !== "none"
          
          // if the lazy image is in the viewport but not visible
          if ( in_viewport && is_not_visible ) {
          
            // replace the lazy image's placeholder with the correct one
            lazyImage.src = lazyImage.dataset.src;
            lazyImage.srcset = lazyImage.dataset.srcset;
            lazyImage.classList.remove("lazy");

            // remove all non-lazy (loaded) images
            lazyImages = lazyImages.filter(function(image) {
              return image !== lazyImage;
            });

            // remove the following event listeners once all lazy images are loaded
            if (lazyImages.length === 0) {
              document.removeEventListener("scroll", lazyLoad);
              window.removeEventListener("resize", lazyLoad);
              window.removeEventListener("orientationchange", lazyLoad);
            }
          }
        });

        // deactivate the loading function
        active = false;
      }, 200);
    }
  };

  // have each event listener activate lazyLoad whenever a user:
  // scrolls the screen
  document.addEventListener("scroll", lazyLoad);
  
  // resizes the screen
  window.addEventListener("resize", lazyLoad);
  
  // changes the screen orientation
  window.addEventListener("orientationchange", lazyLoad);
});

Lazy-load Video

Compared to image data, video is easier due to the smaller quantity of video files uploaded with respect to images. Though video is considerably larger in terms of file size and can cause performance issues on sites hosting it.

We can use the same techniques as images, using:

  • Browser-level lazy-loading
  • IntersectionObserver API
  • Event Handlers

This section will skip over using the IntersectionObserver API and Event Handlers implementation as they were showcased above and only require you to target <video> HTML elements instead of <img> HTML elements.

For browser-level lazy-loading, there are techniques we can use to avoid preloading a video to prevent load times:

<video controls preload="none" poster="one-does-not-simply-placeholder.jpg">
  <source src="one-does-not-simply.webm" type="video/webm">
  <source src="one-does-not-simply.mp4" type="video/mp4">
</video>

We can also use video elements to replace animated GIFs, since short videos usually will have smaller file sizes. We can enable GIF behavior with the following attributes:

<video autoplay muted loop playsinline>
  <source src="one-does-not-simply.webm" type="video/webm">
  <source src="one-does-not-simply.mp4" type="video/mp4">
</video>

The attributes above are self explanatory, except for playsinline which is required for autoplay to work in iOS.

With this article at OpenGenus, you must have the complete idea of Lazy Loading in Web Development.

Sign up for FREE 3 months of Amazon Music. YOU MUST NOT MISS.