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.
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:
- Add an image style to create a lightweight placeholder image
- Add a filter to alter the HTML markup of images
- 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
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.
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.
malabyaMon, 05/25/2020 - 21:42Drupal development, Performance