I was recently looking for a way to leverage lazy loading on a WordPress site while still using the srcset
and custom image size functionality that comes with wp_get_attachment_image()
. I found this article on Google’s Web Dev Fundamentals that describes how to use intersection observer. Lazy Loading Google Guide.
A good question to ask is why should we use Intersection Observer as opposed to what is currently the web standard and WordPress will be adopting in their 5.5 update, loading="lazy"
attribute. Here are a few reasons.
Since part of WordPress’ goal is to use Web Standards, they are inclined to use loading="lazy"
. Although the plan in the future is to adopt Intersection Observer as the Web Standard, there are some things W3C would like to work out first.
Web Standards can take a long time to actually become standard. Take HTML5 for example. In 2012 it was proposed to make HTML5 the standard HTML version and then in 2014 it was put into place as the standard. Two years is a long time to wait for Intersection Observer to become a standard and thus for WordPress Core to adopt it.
loading="lazy"
is less performant than Intersection Observer and less supported. See the screenshot below to see how Intersection Observer is supported on more browsers than loading="lazy"
.
Let’s begin with how to set up the lazy loading functionality using what they discuss in the Google article and then we will hook into the wp_get_attachment_image()
function so that we can utilize it for all the images we get.
The first step is to download intersection observer. Without including the intersection observer script you will run into issues on browsers that don’t directly support it. This is where you can find the JavaScript file.
https://github.com/w3c/IntersectionObserver/blob/master/polyfill/intersection-observer.js
Once it is downloaded we can include it by enqueuing the script.
// Intersection Observer
wp_enqueue_script('intersection-observer', get_template_directory_uri() . '/assets/scripts/intersection-observer.js', array(), filemtime(get_template_directory() . '/assets/scripts/intersection-observer.js'), true);
The first part of the script handles the image tags in the DOM. It creates an array of all the images that have the “lazy” class attached to it. We will address this later on when we hook into the wp_get_attachment_image()
function.
<?php echo wp_get_attachment_image(get_post_thumbnail_id($post->ID), 'medium'); ?>
There are two key elements in the first part of the script. The first is using intersection observer to evaluate if the image section is in view of the user’s window.
The second is the fallback when intersection observer is not working. Specifically, it does not work for Edge 15 and previous versions. For the small users of Edge I have just gone ahead and done all the image src
and srcset
on page load.
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 {
// Fall back to a more compatible method here
// Handles Edge 15 cases
console.log('running Edge fallback for lazy loading');
lazyImages.forEach(function(element) {
element.src = element.dataset.src;
element.srcset = element.dataset.srcset;
element.classList.remove('lazy');
});
}
});
The second part of the script handles all DOM elements that have the class “lazy-background” and a background-image
attribute.
This is an example of an element with the “lazy-background” class and a data-src
attribute.
<div class="lazy-background" data-src="background-image.jpg"></div>
Here we are setting a placeholder background image.
.lazy-background {
background-image: url("hero-placeholder.jpg"); /* Placeholder image */
}
Similar to the first part of the script we are replacing with the data-src attribute but we are instead replacing the background-image
attribute. The Google recommendation in the article mentioned above does a class replacement for the background-image
, but for consistency’s sake I like to use the same method for replacement as the image tag.
We are also using the same fallback for the Edge 15 browser to perform the replacement on DOM load.
// background-image through css
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.style.backgroundImage = "url("+entry.target.dataset.src+")";
lazyBackgroundObserver.unobserve(entry.target);
}
});
});
lazyBackgrounds.forEach(function(lazyBackground) {
lazyBackgroundObserver.observe(lazyBackground);
});
} else {
// Fall back to a more compatible method here
// Handles Edge 15 cases
console.log('running Edge fallback for lazy loading');
lazyBackgrounds.forEach(function(element) {
element.style.backgroundImage = "url("+element.dataset.src+")";
});
}
});
We are going to hook into the wp_get_attachment_image()
function by specifically hooking into the wp_get_attachment_image_attributes
filter.
We are
data-src
attribute and setting it to the the src
value.src
attribute will be set to an empty string.data-srcset
attribute and setting it to the the srcset
value.srcset
attribute will be set to an empty string.This is the filter I use.
function attachment_image_lazy_load($attr) {
if (isset($attr['class'])) {
$attr['class'] = $attr['class'] . ' lazy';
}
if (isset($attr['src'])) {
$attr['data-src'] = $attr['src'];
$attr['src'] = "";
}
if (isset($attr['srcset'])) {
$attr['data-srcset'] = $attr['srcset'];
$attr['srcset'] = "";
}
return $attr;
}
add_filter('wp_get_attachment_image_attributes', 'attachment_image_lazy_load');
There are many ways to implement lazy loading, even plugins such as https://wordpress.org/plugins/rocket-lazy-load/ or https://wordpress.org/plugins/wp-smushit/ can do it for you. What I like about this implementation is there is very little risk that the wp_get_attachment_image_attributes
filter will change substantially causing the function to break. And even if it does change or deprecate the fallback will likely just be that the wp_get_attachment_image()
method will function as normal, since there is little worry of the script making replacements since there is nothing to replace it with.
And since we are including the Intersection Observer script there is little risk of the script we created breaking anything. That is unless the browser stops supporting Intersection Observer, but I see that as unlikely since it’s Google’s current advice on implementing lazy loading and Google essentially owns the space on what is “standard” in browsers.