Lazy loading is a technique that defers the loading of images when it is actually required and not loading them upfront. Thus, images will be loaded only when an image is in the viewport of the browser as the user scrolls. This helps in reducing the initial size of the payload of a webpage and improving the performance.

There is an excellent article Lazy Loading Images and Video which describes how to use Intersection Observer API to register an observer to watch image elements. The whole idea is to load a small lightweight placeholder image upfront on page load and lazily load the original image when the observer detects an image in the viewport. This is similar to the technique used by Medium to load images on their portal.

Image lazy loading in Medium.com
Image lazy loading in Medium. 
Credit: https://developers.google.com/web/fundamentals/performance/lazy-loading-guidance/images-and-video

Using the method described in Lazy Loading Images and Video, to lazy load an inline image in Drupal we will need to do 3 things:

  1. Add an image style to create a lightweight placeholder image
  2. Add a filter to alter the HTML markup of images
  3. Add an intersection observer in JavaScript to lazy load image

Tl;dr

Check out the plugin file here: https://github.com/malabya/imalabya/blob/master/web/modules/custom/im_filters/src/Plugin/Filter/LazyLoad.php

The JavaScript file with the intersection observer: https://github.com/malabya/imalabya/blob/master/web/themes/imalabya/src/scripts/lazyload.es6.js

Add an image style to create a lightweight placeholder image

This step is fairly simple. Create a new image style to generate a small lightweight placeholder image. This placeholder image will be loaded when a page is loaded to show a blurred out image which will be replaced by the intersection observer.

For this, create an image style called placeholder and apply the Scale effect to have the image width of 5px.

Add a filter to alter the HTML markup of images

To lazy load an image we will the image to be in a particular HTML structure


<img class="lazy" src="placeholder.jpg" data-src="image-to-lazy-load.jpg">

Filter API is a great plugin that can be added to text formats which will alter the text before it is being rendered and gets cached. We can also attach libraries to the plugin which will ensure that the intersection observer JavaScript gets loaded only when it is required [more performance improvement].

Start off by defining a Filter plugin using annotations.


/**
 * Lazy load image.
 *
 * @Filter(
 *   id = "lazy_load_image",
 *   title = @Translation("Lazy load inline images."),
 *   type = Drupal\filter\Plugin\FilterInterface::TYPE_MARKUP_LANGUAGE
 * )
 */

The plugin class extends the FilterBase class which has a process method where all the action will happen. In the process method, loop through all img tags and apply the changes required.

To apply the placeholder image style, generate the URL of the image after applying the effects.


$filename = pathinfo(urldecode($src), PATHINFO_BASENAME);
$style = ImageStyle::load('placeholder');
$uri = $style->buildUrl('public://inline-images/' . $filename);
return file_url_transform_relative($uri);

The last thing to do is to add the attributes for the image HTML. In the HTML snippet above there are 3 attributes that need to be added. 

  • The class attribute which will be used as a selector for the javascript
  • The src attribute will hold the placeholder image
  • The data-src attribute will hold the actual image to be loaded.

$src = $element->getAttribute('src');

// Get the placeholder image src.
$placeholder = $this->getPlaceholder($src);

// Get any existing classes and add the lazy class for lazy loading image.
$classes = explode(" ", $element->getAttribute('class'));
array_push($classes, 'lazy');

// Set the attributes.
$element->setAttribute('class', implode(" ", $classes));
$element->setAttribute('src', $placeholder);
$element->setAttribute('data-src', $src);

Add an intersection observer in JavaScript to lazy load image

Intersection Observer API is a relatively new API in browsers that is really simple to detect when an element enters the viewport and take an action when it does. 

The javascript will fetch all the img tags in the dom with lazy class and when IntersectionObserver is in the viewport, it will replace the src of the image with the path set in the data-src. There is a fallback for browsers that doesn't support IntersectionObserver


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

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

    lazyImages.forEach(function (lazyImage) {
      lazyImageObserver.observe(lazyImage);
    });
  } else {
    // Fall back to a more compatible method
    lazyImages.forEach(function (lazyImage) {
      lazyImage.src = lazyImage.dataset.src;
    });
  }
});

Lazy loading in action

Once, the plugin and the javascript is implemented and the filter is enabled on a text format check, the lazy load functionality will kick in for all the images being uploaded in CKEditor.

To check how if lazy loading actually working, watch the below gif animation.

Lazy loading demo
Give it a little time, it's a big image.

As you can see in the initial stage, only the placeholder images are loaded which are pretty lightweight ~750B in size. As I scroll down the page, the IntersectionObserver comes into play and loads the original image when the image comes in the viewport of the browser. So, even though the final page is over ~3.5MB the initial load of the webpage is a little over 1.5KB.

Conclusion

Lazy loading, if implemented properly can significantly improve the page speed by loading the necessary content of a page and defer loading the big image assets when it is required. All done keeping the content intact. With faster loading of pages, the user experience will also improve greatly which users will love.

Down the lane, I am planning to build a Drupal module out of it to contribute it back to the community so that more people can use this on their sites and improve the performance.