DEV Community

Cover image for How to preload images in WordPress
Jackson Lewis
Jackson Lewis

Posted on

How to preload images in WordPress

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



/**
 * 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' );


Enter fullscreen mode Exit fullscreen mode

Breakdown

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



if ( ! is_singular() ) {
    return;
}


Enter fullscreen mode Exit fullscreen mode

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.



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


Enter fullscreen mode Exit fullscreen mode

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.



$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 = '';


Enter fullscreen mode Exit fullscreen mode

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.



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


Enter fullscreen mode Exit fullscreen mode

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.



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


Enter fullscreen mode Exit fullscreen mode

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, 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:



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


Enter fullscreen mode Exit fullscreen mode

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

To make this comparison, I've used the Filmstrip on 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 on Unsplash

Top comments (7)

Collapse
 
christopherbio profile image
christopher-bio • Edited

Thank you so much for this outstanding code!

For those of you using Web P Express without automatic conversion that want to check, whether an image exists, before replacing the URLs:

Add this function:
function get_webp_url_if_available_own($url){
$webp_url = $url . ".webp";
$webp_url_folder = str_replace("/uploads/", "/webp-express/webp-images/doc-root/wp-content/uploads/", $webp_url);

if (@getimagesize($webp_url_folder)) {
$url = $webp_url_folder;
}
return $url;
}

and this part after the srcset has been initialed:
if( strpos( $_SERVER['HTTP_ACCEPT'], 'image/webp' ) !== false ) {
// webp is supported!
$src = get_webp_url_if_available_own($src);
$src_split = explode(", ", $srcset);

foreach ($src_split as $key => $src_single) {
$src_single_split = explode(" ", $src_single);
$single_img = $src_single_split[0];
$single_img = get_webp_url_if_available_own($single_img);
$src_single_split[0] = $single_img;
$src_split[$key] = implode(" ", $src_single_split);
}

$srcset = implode(", ", $src_split);
}

Hope it works for you and would appreciate feedback, if it does not.

Collapse
 
bettylex profile image
Bettylex • Edited

Thank you very much for this wonderful code! It's exactly what I've been looking for and couldn't find anywhere.

For those of you using Webp Express, I've found a workaround:

Replacing: "printf( '', esc_url( $src ), $additional_attr );"

With: "if( strpos( $_SERVER['HTTP_ACCEPT'], 'image/webp' ) !== false ) {
$srcw = str_replace( '.jpg','.jpg.webp', $src );
$additional_attrw = str_replace( '.jpg','.jpg.webp', $additional_attr );
printf( 'link rel="preload" as="image" href="%s" %s/', esc_url( $srcw ), $additional_attrw );

} else { printf( 'link rel="preload" as="image" href="%s" %s/', esc_url( $src ), $additional_attr ); }"

Str_replace can be modified to add more image formats.

I know it's not the cleanest solution, but I couldn't find any other. I haven't checked yet but, if using a CDN like Cloudflare or a preloading cache function like WP Rocket's, it may require separate webp caching to work cross browser.

PS: I hade to remove the "<" and ">" chars in the link tag so that it doesn't disappear in this comment. Restore them before adding this to your functions.php.

Collapse
 
discovideos profile image
John Nobody • Edited

Its really useful script, but my scr looks weird (because webp), can you help with that case, please?..
dev-to-uploads.s3.amazonaws.com/up...

Collapse
 
rogerm profile image
RogerM

Do we need to input anything into this code snippet or just copy/paste into the function.php file?

Collapse
 
jonnathanmonsalve profile image
Jonnathan Monsalve

This is great! Exactly what I was looking for. Thank you!!

Collapse
 
amitsingh54325 profile image
The Nationalist • Edited

Hi Jackson,

To make this script work on wordpress, where should I Place it ?

Best Regards
Amit

Collapse
 
jacksonlewis profile image
Jackson Lewis

Hey, so you should place this within your functions.php file in your theme.