Connor Smyth

Lazy Loading Images in WordPress

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.

Why Use Intersection Observer

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

Intersection Observer browser support
Intersection Observer browser support
loading="lazy" browser support
loading=”lazy” browser support

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.

Intersection Observer

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);

Lazy Loading Script

Part 1

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');
        });
    }
});

Part 2

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+")";
        });
    }
});

WP Get Attachment Image

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

  • Adding the “lazy” class to the image
  • Adding the data-src attribute and setting it to the the src value.
  • Then the src attribute will be set to an empty string.
  • Adding the data-srcset attribute and setting it to the the srcset value.
  • Then the 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');

Conclusion

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.