We'll show you all the aspects that make the process of deferring images, otherwise known as lazy loading, so effective. We'll also talk about different ways we can utilize it, such as native lazy loading or advanced JavaScript techniques with Intersection Observer API.
Are you trying to improve your website’s performance and you came across a warning that says something about deferring offscreen images?
In this post, we’re going to talk about how to go through this process. First of all, PageSpeed Insights will include a list of all offscreen images in its report. This gives us an opportunity to improve Time To Interactive (TTI) score, and images we need to target with our lazy loading techniques.
What is offscreen image deferring
More commonly known as lazy loading, it’s a process where the browser postpones the image load to when it’s needed. In other words, images that appear outside the viewport, don’t load during the initial page load.
How to defer offscreen images
In case you’re a WordPress user, it already provides you with lazy loading functionality by default. However, you’ll need to have version 5.4 or higher installed, since that’s when it was introduced.
Native lazy loading
This is a manual way of deferring images. Moreover, you’ll need to add the loading attribute to your images and set its value to “lazy”. If you go with this option, you should also avoid doing it for the images that appear above the fold during initial page load.
JavaScript Solutions
We’re going to describe two methods for implementing lazy loading with JavaScript. With the first one, we’ll work with event handlers and with the other, we’ll introduce the Intersection Observer API. And finally, we’re going to show you how to defer CSS background images, which has a slightly different twist to it.
In general, browsers start to load images immediately after they find a valid image source link in the src attribute of the img tag. In order to prevent them from starting loading images on initial page load, we need to remove the src attribute and create another arbitrary attribute like data-src, which will hold the image source link. In short, if the browser doesn’t find the src attribute in the img tag, it won’t load the image.
JavaScript Event Handlers
With this method, we can use JavaScript event handlers such as scroll, resize and orientationChange to add the src attribute to img tag and set its value to the actual image source link. To clean it up, you can remove the data-src attribute after, since we won’t need it anymore. Furthermore, to limit the selection of images you want to defer, you should add a special class to them, which you’ll remove after the image loads.
The following snippet shows how to do it in practice.
document.addEventListener("DOMContentLoaded", function() {
var lazyloadImages = document.querySelectorAll("img.lazy");
var lazyloadThrottleTimeout;
function lazyload () {
if(lazyloadThrottleTimeout) {
clearTimeout(lazyloadThrottleTimeout);
}
lazyloadThrottleTimeout = setTimeout(function() {
var scrollTop = window.pageYOffset;
lazyloadImages.forEach(function(img) {
if(img.offsetTop < (window.innerHeight + scrollTop)) {
img.src = img.dataset.src;
img.classList.remove('lazy');
}
});
if(lazyloadImages.length == 0) {
document.removeEventListener("scroll", lazyload);
window.removeEventListener("resize", lazyload);
window.removeEventListener("orientationChange", lazyload);
}
}, 20);
}
document.addEventListener("scroll", lazyload);
window.addEventListener("resize", lazyload);
window.addEventListener("orientationChange", lazyload);
});
Intersection Observer API
The other method we mentioned above is with the Intersection Observer API. Moreover, it provides us a way to asynchronously observe changes in the intersection of a target element with document’s viewport. In other words, it makes it simple to detect when a certain element enters the viewport.
Similarly to the previous method, we can limit the selection of the images by adding a special class to them and use the query selector to select images with that class. You’ll also need to create an Observer instance, which you can then use to add the missing src attribute and remove the data-src attribute.
If we compare this method to the previous one with the event handlers, we can see that it’s faster. Moreover, images load faster with an Observer and the action to load the image is triggered quicker as well.
The following snippet shows how to implement it.
document.addEventListener("DOMContentLoaded", function() {
var lazyloadImages;
if ("IntersectionObserver" in window) {
lazyloadImages = document.querySelectorAll(".lazy");
var imageObserver = new IntersectionObserver(function(entries, observer) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
var image = entry.target;
image.src = image.dataset.src;
image.classList.remove("lazy");
imageObserver.unobserve(image);
}
});
});
lazyloadImages.forEach(function(image) {
imageObserver.observe(image);
});
} else {
var lazyloadThrottleTimeout;
lazyloadImages = document.querySelectorAll(".lazy");
function lazyload () {
if(lazyloadThrottleTimeout) {
clearTimeout(lazyloadThrottleTimeout);
}
lazyloadThrottleTimeout = setTimeout(function() {
var scrollTop = window.pageYOffset;
lazyloadImages.forEach(function(img) {
if(img.offsetTop < (window.innerHeight + scrollTop)) {
img.src = img.dataset.src;
img.classList.remove('lazy');
}
});
if(lazyloadImages.length == 0) {
document.removeEventListener("scroll", lazyload);
window.removeEventListener("resize", lazyload);
window.removeEventListener("orientationChange", lazyload);
}
}, 20);
}
document.addEventListener("scroll", lazyload);
window.addEventListener("resize", lazyload);
window.addEventListener("orientationChange", lazyload);
}
})
Lazy-loading CSS background images
When you want to lazy load an image that’s set inside the CSS, you’ll first need to prevent it from loading on an initial page load. We can do this by adding a special class to the element which displays the image. Furthermore, we need to override the background-image property and set it to none. After that, we can use JavaScript to remove this class from the element once it needs to get loaded.
The following snippet shows how to implement it.
JS:
document.addEventListener("DOMContentLoaded", function() {
var lazyloadImages;
if ("IntersectionObserver" in window) {
lazyloadImages = document.querySelectorAll(".lazy");
var imageObserver = new IntersectionObserver(function(entries, observer) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
var image = entry.target;
image.classList.remove("lazy");
imageObserver.unobserve(image);
}
});
});
lazyloadImages.forEach(function(image) {
imageObserver.observe(image);
});
} else {
var lazyloadThrottleTimeout;
lazyloadImages = document.querySelectorAll(".lazy");
function lazyload () {
if(lazyloadThrottleTimeout) {
clearTimeout(lazyloadThrottleTimeout);
}
lazyloadThrottleTimeout = setTimeout(function() {
var scrollTop = window.pageYOffset;
lazyloadImages.forEach(function(img) {
if(img.offsetTop < (window.innerHeight + scrollTop)) {
img.src = img.dataset.src;
img.classList.remove('lazy');
}
});
if(lazyloadImages.length == 0) {
document.removeEventListener("scroll", lazyload);
window.removeEventListener("resize", lazyload);
window.removeEventListener("orientationChange", lazyload);
}
}, 20);
}
document.addEventListener("scroll", lazyload);
window.addEventListener("resize", lazyload);
window.addEventListener("orientationChange", lazyload);
}
})
CSS:
#bg-image.lazy {
background-image: none;
background-color: #F1F1FA;
}
#bg-image {
background-image: url("https://ik.imagekit.io/demo/img/image10.jpeg?tr=w-600,h-400");
max-width: 600px;
height: 400px;
}
Improve the User Experience with Lazy Loading
We already established that lazy loading greatly benefits the overall performance of a website, which also improves the user experience. However, it doesn’t end there. We still have a few tricks up our sleeves to improve upon the user experience even further.
Avoid large layout shifts
One of the most important things we need to look out for when we’re implementing lazy loading, are layout shifts. These occur when we don’t reserve the space for our images while they haven’t loaded yet. And as they load, they cause the content around them to jump around. This is not ideal for user experience and may as well be the main reason for a large bounce rate.
To solve this issue, we simply need to set the dimensions attributes to images or dimension properties to their container elements. You can also read more about resolving large layout shifts [link to avoid large layout shifts article] in another one of our articles, where we delve even deeper into resolving this issue.
Avoid lazy loading above the fold images
Lazy loading of every image is not always good for the website performance and user experience. Generally, we want the content that appears first when the browser loads a page to load without any delays. Therefore, we should avoid lazy loading images that appear above the fold. We should also take into consideration that different devices can fit different amounts of images on their screens. To clarify, a website will show more images on initial load when we open it on a desktop environment than it will when we do it on mobile.
Start loading images before they enter the viewport
By default, deferred images will start loading when they enter the viewport. Therefore, we are still able to see empty spaces or placeholders if we scroll fast enough. To solve this issue, we can add a margin to their load trigger point. This will cause them to start loading when we scroll close to them, before they enter the viewport.
With Intersection Observer API, we can use root and rootMargin parameters to set this up. In case you’re working with event handlers, you can simply use a positive number for the difference between image edge and the viewport edge positions.
Using an appropriate placeholder
Instead of having just an empty space wherever the image is going to load, we can use placeholders. While it’s common to have a dull universal placeholder for every image, there certainly is room for improvement.
Option 1: Dominant Color Placeholder
As the name of this option might suggest, we’ll use the dominant color of the original image as the color of our placeholders. Some of the more major websites also use this technique, such as Google and Pinterest.
In order to find the dominant color from an image, we can use color quantization technique from the GraphicsMagick npm package.
The following snippet shows how to do it.
var gm = require('gm');
gm('test.jpg')
.resize(250, 250)
.colors(1)
.toBuffer('RGB', function (error, buffer) {
console.log(buffer.slice(0, 3));
});
Option 2: Low quality image placeholders
We can use a very low quality, blurred version of the original image as a placeholder. To save as much bandwidth as possible, we need to convert the image into WebP format [link to serving images in modern format article] with the worst possible quality, blur it, and resize it. This technique is also used by some of the major websites such as Facebook and Medium.
Conclusion
To wrap it up, we tackled the “Defer offscreen images” issue the PageSpeed Insights points to. Moreover, we went down the rabbit hole and explored how exactly deferring images works and the various different ways we can implement it. While it’s a very beneficial technique to improve overall website performance, it still needs to be limited to offscreen images on initial load, otherwise it may weigh down on the user experience.