How to preload images in WordPress
Tags: wordpress, performance, preload, images
Published: 2021-04-19 16:11:54

Isn't it fun, the never ending game of web performance! This post will be looking using `preload` on images, and more specifically at preloading the featured image of a WordPress post. So let's get stuck in.

## Preload featured image of a post

```php
/**
 * Preload attachment image, defaults to post thumbnail
 */
function preload_post_thumbnail() {
    global $post;
    /** Prevent preloading for specific content types or post types */
    if ( ! is_singular() ) {
        return;
    }
    /** Adjust image size based on post type or other factor. */
    $image_size = 'full';

    if ( is_singular( 'product' ) ) {
        $image_size = 'woocommerce_single';

    } else if ( is_singular( 'post' ) ) {
        $image_size = 'large';

    }
    $image_size = apply_filters( 'preload_post_thumbnail_image_size', $image_size, $post );
    /** Get post thumbnail if an attachment ID isn't specified. */
    $thumbnail_id = apply_filters( 'preload_post_thumbnail_id', get_post_thumbnail_id( $post->ID ), $post );
    
    /** Get the image */
    $image = wp_get_attachment_image_src( $thumbnail_id, $image_size );
    $src = '';
    $additional_attr_array = array();
    $additional_attr = '';

    if ( $image ) {
        list( $src, $width, $height ) = $image;

        /**
         * The following code which generates the srcset is plucked straight
         * out of wp_get_attachment_image() for consistency as it's important
         * that the output matches otherwise the preloading could become ineffective.
         */
        $image_meta = wp_get_attachment_metadata( $thumbnail_id );
 
        if ( is_array( $image_meta ) ) {
            $size_array = array( absint( $width ), absint( $height ) );
            $srcset     = wp_calculate_image_srcset( $size_array, $src, $image_meta, $thumbnail_id );
            $sizes      = wp_calculate_image_sizes( $size_array, $src, $image_meta, $thumbnail_id );

            if ( $srcset && ( $sizes || ! empty( $attr['sizes'] ) ) ) {
                $additional_attr_array['imagesrcset'] = $srcset;

                if ( empty( $attr['sizes'] ) ) {
                    $additional_attr_array['imagesizes'] = $sizes;
                }
            }
        }

        foreach ( $additional_attr_array as $name => $value ) {
            $additional_attr .= "$name=" . '"' . $value . '" ';
        }

    } else {
        /** Early exit if no image is found. */
        return;
    }

    /** Output the link HTML tag */
    printf( '<link rel="preload" as="image" href="%s" %s/>', esc_url( $src ), $additional_attr );
}
add_action( 'wp_head', 'preload_post_thumbnail' );
```

### Breakdown

There's a fair bit going on here, so let's break it down and go through the process.

```php
if ( ! is_singular() ) {
    return;
}
```

The first `if` statement aims to prevent any preloading from taking place if any given condition is met. Perhaps you don't want to preload the featured image of a post that belongs to a certain post type, or just a specific page template.

```php
$image_size = 'full';

if ( is_singular( 'product' ) ) {
    $image_size = 'woocommerce_single';

} else if ( is_singular( 'post' ) ) {
    $image_size = 'large';

}
$image_size = apply_filters( 'preload_post_thumbnail_image_size', $image_size, $post );
```

Here we set the size of the image we want to load. This is actually very important, because if we preload the wrong size image, we waste user data. It's worth noting that auditing tools like Lighthouse will probably scream at you too! So it's vital that the image size requested here matches the size requested on the corresponding template.

You could be fancy and create some form of an API, where this function to preload and the function that outputs the image tag are in the same place such as within a class. It could allow you to define in one place the image size and enforce consistency.

```php
$thumbnail_id = apply_filters( 'preload_post_thumbnail_id', get_post_thumbnail_id( $post->ID ), $post );
    
/** Get the image */
$image = wp_get_attachment_image_src( $thumbnail_id, $image_size );
$src = '';
$additional_attr_array = array();
$additional_attr = '';
```

Next we set the ID of the attachment, which by default is that of the featured image (also known as the thumbnail). We then call `wp_get_attachment_image_src()`, passing our `$thumbnail_id` and `$image_size`. You'll see we also setup some variables which we'll be using in the next step.

```php
if ( $image ) {
    list( $src, $width, $height ) = $image;

    /**
     * The following code which generates the srcset is plucked straight
     * out of wp_get_attachment_image() for consistency as it's important
     * that the output matches otherwise the preloading could become ineffective.
     */
    $image_meta = wp_get_attachment_metadata( $thumbnail_id );

    if ( is_array( $image_meta ) ) {
        $size_array = array( absint( $width ), absint( $height ) );
        $srcset     = wp_calculate_image_srcset( $size_array, $src, $image_meta, $thumbnail_id );
        $sizes      = wp_calculate_image_sizes( $size_array, $src, $image_meta, $thumbnail_id );

        if ( $srcset && ( $sizes || ! empty( $attr['sizes'] ) ) ) {
            $additional_attr_array['imagesrcset'] = $srcset;

            if ( empty( $attr['sizes'] ) ) {
                $additional_attr_array['imagesizes'] = $sizes;
            }
        }
    }

    foreach ( $additional_attr_array as $name => $value ) {
        $additional_attr .= "$name=" . '"' . $value . '" ';
    }

} else {
    /** Early exit if no image is found. */
    return;
}
```

This step is a little chunkier that the others, but what's going on is fairly simple. Most of what you see here is directly copied out of the core function `wp_get_attachment_image()`, the function used to output the `<img>` tag of an image. So, what it does is it calculates the `srcset` and `sizes` for the image, based on the `$image_size` provided. The reason we're using almost a direct copy of the code is simple, consistency. It's very important we have that when working with preload. From here we build up an array of HTML attributes which will be used in the final step.

```php
printf( '<link rel="preload" as="image" href="%s" %s/>', esc_url( $src ), $additional_attr );
```

With the image we want, and having collated the necessary attribute values based on the image size, we can output the `<link>` tag to preload the image.

### Filters

You'll notice there are two filters, one for the `$thumbnail_id`, and another for the `$image _size`. The purpose of these is that it is entirely possible for an image that isn't the featured image to be important enough that it should be preloaded. These filters allow you to change the ID of the attachment that is to be preloaded.

If you were using this directly in a theme for instance, you wouldn't need to use them, you could directly modify the function. I just wanted to show how it would be done if this were a plugin or part of a theme you couldn't actively modify.

## Use preload responsibly

Now before you go and try to preload for yourself, it's important we quickly understand what the concept of preload is and how exactly it works. Preload lets you tell the browser what resources are critical and should be fetched as soon as possible, without having to wait for them to be discovered in the HTML, CSS or JS. You can learn more in-depth with [this article on web.dev](https://web.dev/preload-responsive-images/), as I want to focus more on using preload in WordPress.

If you try to preload too many resources, the concept of preload almost becomes obsolete because if you define everything as important, it all automatically becomes not important...

### Usage of preload for WordPress

From our function above, we're only using it to preload the featured image of a post. As the majority of the time, any image above the fold is most likely to be just that. However, that's not to say there won't be instances where there's an image above the fold that isn't a featured image. In this scenario, we could utilize the filters as mentioned earlier to change the attachment ID of the image we want to use. For example, maybe for a template you're not using the built-in featured image, but an ACF image field instead. Well you could simply do something like the following:

```php
add_filter( 'preload_post_thumbnail_id', function( $thumbnail_id, $post ) {

    if ( get_page_template_slug( $post ) == 'templates/home.php' ) {
        $thumbnail_id = get_field( 'banner_image' );
    }

    return $thumbnail_id;
}, 10, 2 );
```

In the above code snippet, if the page has a template with the slug `templates/home.php`, we change the `$thumbnail_id` to use the value from an ACF field instead.

### Preload multiple images?

This is a question which may have sprung to mind. In this case, you may want to abstract much of the function so that you can run it multiple times. Perhaps you'd want to create an array of associative arrays, and loop over that and output the `<link>` to each image. I won't get into that for this post, although I could add it in if you would find it beneficial!

## The results

Now for the important question, did it actually make a difference? Well, yes! Below we have a comparison, the before is, well, before preloading the featured image. And after is, obviously, after we added preloading.

![Screenshot of a before and after showing frames of a web page loading](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qeqn9cakdpi9o1770ax5.jpg)

To make this comparison, I've used the Filmstrip on [webpagetest.org](https://www.webpagetest.org). The test was with Mobile on a slow 3G connection. As, when it comes to performance, these are the network connection speeds where it really matters.

In each frame, the ones with the red border are when the **Largest Contentful Paint** fully rendered on the page. You can see preloading the image made the image fully render on the page a whomping 4.5 seconds sooner! You may also notice that when preloading was added, the LCP image rendered in all at once, as opposed to progressively loading as seen before. This is because, when it came to rendering the image, it had already been fully downloaded, so was just a matter of rendering.

However, during my testing I noticed a downside, and that was that it prioritised fetching the image over critical stylesheets. This ultimately increased the time before anything was rendered in the page, commonly known as **First Contentful Paint**. So it's important to be mindful of any potential side effects when preloading resources, as you could be doing more harm then good!

Photo by [John Baker](https://unsplash.com/@jlondonbaker?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com/s/photos/rocket?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)