Using WebP Thumbnails on WordPress Sites

Published

A few months ago I wrote a general post about incorporating WebP images into websites. The post made the case for using WebP (i.e. improved performance), and gave HTML syntax showing how it could be included in a website.

As far as specifics for incorporating WebP images into a WordPress theme, I still had some work to do. The solution I ultimately ended up with is described below, along with the lengthy bit of PHP code required!

WordPress Specific Solution

Incorporating WebP images into my WordPress theme depended on two main components:

  1. Creating the WebP thumbnails – I used a WordPress Plugin called EWWW Image Optimizer to create WebP thumbnails for newly uploaded images. This plugin was paired with the Regenerate Thumbnails plugin, which added the ability to go back and create WebP thumbnails for my existing images.
  2. Inserting HTML Picture Elements with custom PHP – WebP is best included as part of an HTML <picture> element for backwards compatibility. As of June 2023, WordPress does not perform multi-source <picture> element creation out of the box, and custom PHP was required.

While this is the route I took, it is worth mentioning that many image optimization plugins offer the ability to use Apache/Nginx re-write rules to have the webserver serve WebP thumbnails even when .jpg or .png images are requested. This is an alterative to number 2 above, and may be simpler depending on your case. Documentation and examples describing this method are readily available.

1. Creating the WebP Thumbnails

I used the EWWW Image Optimizer plugin to create the WebP thumbnails. Configuration was straightforward, and just involved confirming WebP Conversion was enabled.

Confirming WebP Conversion is enabled for new uploads

Converting Existing Images

In my case, I had a couple hundred existing images that had already been uploaded to WordPress. I was able to use the Regenerate Thumbnails plugin to go back and create WebP renditions of these existing images in batch.

WebP thumbnails could be created for existing images in batch
WebP thumbnails could also be creating for individual images

Once the WebP thumbnails had been created, I was ready to include them in the website HTML.

2. Inserting HTML Picture Elements with custom PHP

As of June 2023, WordPress outputs images using the <img /> tag. This works great to responsive images and source sets, but doesn’t provide an easy mechanism for progressive enhancement, specifically: serving WebP images to browsers that support them, and falling back to regular JPEG images otherwise.

Research in my other article concluded that the HTML <picture> element was the best way to insert WebP renditions while still providing backwards compatibility for older browsers.

Replacing Img HTML Tags with Picture Tags

I created a custom function to search through post content and replace <img /> tags with <picture> elements. This function was added to my theme’s functions.php file, an executed as part of the the_content filter.

The custom code ended up being quite lengthy, and included logic for:

  • Creating <picture> tags with both webp and jpeg/png <source> elements
  • Confirming webp thumbnails existed for an image before including them (sometimes EWWW Image Converter would fail for a certain image/size)
  • Creating image specific sizes and srcset attributes depending on available thumbnails and the width attribute of the image (set in the WordPress Gutenberg editor)
function replace_content_img_with_picture($content){

  $postContent = new DOMDocument();

  // ensure HTML entities are encoded properly in output
  //  https://stackoverflow.com/questions/8218230/php-domdocument-loadhtml-not-encoding-utf-8-correctly
  $encodedContent = mb_convert_encoding($content, 'HTML-ENTITIES', 'UTF-8');

  // parse content HTML into DOMDocument and wrap in section without html and body wrapper tags
  //  - https://stackoverflow.com/questions/4879946/how-to-savehtml-of-domdocument-without-html-wrapper
  //  - LIBXML_HTML_NOIMPLIED requires a root note as mentioned in https://stackoverflow.com/a/36547335
  @$postContent->loadHTML("
{$encodedContent}
", LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); // loop through all the tags $imgs = $postContent->getElementsByTagName('img'); foreach( $imgs as $img ) { // get attachment id from img class (e.g. parsing class string "wp-image-4991" into "4991") // - this is done so we can look-up attachment metadata to determine which sizes are available $attachmentId = -1; $imgClass = $img->getAttribute('class'); if (isset($imgClass)) { $imgClassArray = explode(" ", $imgClass); if (count($imgClassArray) > 0) { foreach ($imgClassArray as &$imgClassItem) { if (str_starts_with($imgClassItem, "wp-image-")) { $attachmentId = (int)substr($imgClassItem, 9); break; } } } } if ($attachmentId getAttribute('width'); // assemble available thumbnail sizes into array $thumbnails = array(); $thumbnailsObject = $attachmentMetadata['sizes']; foreach($thumbnailsObject as $thumbnailName => $thumbnail) { if ($thumbnailName === 'thumbnail') continue; // remove 150px thumbnail rendition $thumbnail['name'] = $thumbnailName; // used later when calling wp_get_attachment_image_src() array_push($thumbnails, $thumbnail); } // sort in ascending order of width to ensure smallest image source added first usort($thumbnails, function ($thumbnailA, $thumbnailB) use ($order) { return $thumbnailA['width'] - $thumbnailB['width']; }); // (optional) add original file as thumbnail if none present or for small images with limited renditions (edge case) // note: conditional is somewhat arbitrary, trying to target small images $largestThumbnail = $thumbnails[array_key_last($thumbnails)]; if (!isset($largestThumbnail) || ($attachmentMetadata['width'] > $largestThumbnail['width'] && $largestThumbnail['width'] basename($attachmentMetadata['file']), "width" => $attachmentMetadata['width'], "height" => $attachmentMetadata['height'], "mime-type" => wp_check_filetype($attachmentMetadata['file'])['type'], "filesize" => $attachmentMetadata['filesize'], "name" => 'full' // works with wp_get_attachment_image_src() ]; array_push($thumbnails, $originalThumbnail); } // create picture element that will contain source and img elements $pictureElement = $postContent->createElement('picture'); // ensure webp thumbnail files exist before including them - sometimes EWWW Image Optimized failed to create webp thumbnails $webpThumbnails = array(); foreach ($thumbnails as $thumbnail) { $webpThumbnailName = "{$attachmentParentFolder}/{$thumbnail['file']}.webp"; if (file_exists($webpThumbnailName)) { array_push($webpThumbnails, $thumbnail); } } // source (webp) if (count($webpThumbnails) > 0) { $webpSourceElement = $postContent->createElement('source'); decorateImageElement($webpSourceElement, 'image/webp', $webpThumbnails, $attachmentId, $imgWidthAttribute); $pictureElement->appendChild($webpSourceElement); } // img (original) if (count($thumbnails) > 0) { $imgElement = $postContent->createElement('img'); decorateImageElement($imgElement, $thumbnails[0]['mime-type'], $thumbnails, $attachmentId, $imgWidthAttribute); // move over any original attributes, skipping sizes and srcset foreach ($img->attributes as $attr) { if ($attr->nodeName === 'sizes' || $attr->nodeName === 'srcset') continue; $imgElement->setAttribute($attr->nodeName, $attr->nodeValue); } $pictureElement->appendChild($imgElement); } // insert picture element and remove original img element $img->parentNode->insertBefore($pictureElement, $img); $img->parentNode->removeChild($img); } return $postContent->saveHTML(); } add_filter('the_content', 'replace_content_img_with_picture', 9999); function decorateImageElement(&$element, $mimeType, $thumbnails, $attachmentId, $imgWidthAttribute) { $element->setAttribute('type', $mimeType); $sizesAttribute = ""; $srcsetAttribute = ""; $addedMaxSize = false; foreach ($thumbnails as $thumbnail) { $maxWidth = $thumbnail['width']; // generate sizes attribute - reported size will be no larger than img width attribute $thumbnailLargerThanImageWidth = isset($imgWidthAttribute) && $thumbnail['width'] >= $imgWidthAttribute; $renderedWidth = $thumbnailLargerThanImageWidth ? $imgWidthAttribute : $thumbnail['width']; if (next($thumbnails) == true && !$thumbnailLargerThanImageWidth) { $sizesAttribute .= "(max-width: {$maxWidth}px) {$renderedWidth}px, "; } else if (!$addedMaxSize) { $sizesAttribute .= "{$renderedWidth}px, "; $addedMaxSize = true; } $thumbnailUrl = wp_get_attachment_image_src($attachmentId, $thumbnail['name']); if ($mimeType === 'image/webp') { $srcsetAttribute .= "{$thumbnailUrl[0]}.webp {$thumbnail['width']}w, "; } else { $srcsetAttribute .= "{$thumbnailUrl[0]} {$thumbnail['width']}w, "; } } $sizesAttribute = rtrim($sizesAttribute,", "); $srcsetAttribute = rtrim($srcsetAttribute,", "); $element->setAttribute('sizes', $sizesAttribute); $element->setAttribute('srcset', $srcsetAttribute); }

The Resulting HTML

The PHP snippet above generates the following HTML structure, which includes webp and jpg/png sources, along with responsive image source sets.

<picture>
   <source type="image/webp" sizes="(max-width: 660px) 600px, (max-width: 1084px) 1024px, 1200px" 
     srcset="https://taylor.callsen.me/wp-content/uploads/2023/04/tcallsen-pallisades-fingers-mar-2023-400x300.jpg.webp 400w, 
             https://taylor.callsen.me/wp-content/uploads/2023/04/tcallsen-pallisades-fingers-mar-2023-768x576.jpg.webp 768w, 
             https://taylor.callsen.me/wp-content/uploads/2023/04/tcallsen-pallisades-fingers-mar-2023-1024x768.jpg.webp 1024w, 
             https://taylor.callsen.me/wp-content/uploads/2023/04/tcallsen-pallisades-fingers-mar-2023-1536x1152.jpg.webp 1536w, 
             https://taylor.callsen.me/wp-content/uploads/2023/04/tcallsen-pallisades-fingers-mar-2023-2048x1536.jpg.webp 2048w">
   <img type="image/jpeg" sizes="(max-width: 660px) 600px, (max-width: 1084px) 1024px, 1200px" 
     srcset="https://taylor.callsen.me/wp-content/uploads/2023/04/tcallsen-pallisades-fingers-mar-2023-400x300.jpg 400w, 
             https://taylor.callsen.me/wp-content/uploads/2023/04/tcallsen-pallisades-fingers-mar-2023-768x576.jpg 768w, 
             https://taylor.callsen.me/wp-content/uploads/2023/04/tcallsen-pallisades-fingers-mar-2023-1024x768.jpg 1024w, 
             https://taylor.callsen.me/wp-content/uploads/2023/04/tcallsen-pallisades-fingers-mar-2023-1536x1152.jpg 1536w, 
             https://taylor.callsen.me/wp-content/uploads/2023/04/tcallsen-pallisades-fingers-mar-2023-2048x1536.jpg 2048w"
     src="https://taylor.callsen.me/wp-content/uploads/2023/04/tcallsen-pallisades-fingers-mar-2023-1024x768.jpg">
</picture>

3. Confirming Results in Lighthouse

Once the <picture> elements above were being included in my website HTML, I ran a Google Lighthouse audit (via PageSpeed Insights) on one of my posts, and confirmed that the “Serve images in next-gen formats” audit was passing.

Subscribe by Email

Enter your email address below to be notified about updates and new posts.


Comments

Loading comments..

No responses yet

Leave a Reply

Your email address will not be published. Required fields are marked *