Kieron Codes

Automatically serving WebP images in Umbraco

Make your Umbraco site faster by automatically serving images in WebP format, using Image Processor events

January 17, 2021

Why WebP?

WebP is a well-supported image format (with approximately ~80% support) and can often provide up to an additional 30% reduction in size over a typical JPEG. I use this approach on every site that I work on because it works both lossy and losslessly, supports alpha channels which protects your PNGs, and combines really well with some form of lazy loading. These technologies combine to create a fantastic image experience on your site with minimal effort.

You can even use this technique to get a better Lighthouse score on your Umbraco site, as one of the criteria Google grade you against, is the use of Next Gen Image Formats, such as WebP. With this in mind, there is a theory that by pleasing Google in this way, you get a better score with Lighthouse, and thus you may even help your rankings! But I have no evidence for this, still, its good to get a better score with the Lighthouse Audit.

You can read in more detail about it on Google’s page for it here.

I span up a fresh version of Umbraco 8.2.1, and then loaded the default starter kit, and then recorded the weight of the products page, which as you can see from the images below, the first (using JPEG) is around 2.43MB, compare this to the same page and same content, but with the images being served as WebP, (the image after) we come down to 953kb! Furthermore, if you add a quality parameter of 80, that comes down even further to 504kb, for images that look just as good as the originals! No brainer.

Setup

Umbraco ships with Image Processor as part of the core, it’s built by James South. however, WebP is an additional plugin that will need to be loaded in separately. It’s on NuGet here or simply Install-Package ImageProcessor.Plugins.WebP – just double-check the dependencies before you install anything!
It is worth noting that WebP also requires your project to have ImageProcessor.Web installed. Visual Studio offers to install this for you when you start using code that requires it, so this should be straightforward. Alternatively, that’s here, and also available via Install-Package ImageProcessor.Web

Its this package that we use to hook into the event that we need for this to work before we then rely on the WebP plugin for the conversion.

V8

In Umbraco v7 we relied upon IApplicationEventHandler to bootstrap the code, but in Umbraco v8 we have to use Composers and Components instead.

The steps that we are going to follow are:

  1. Append our component to the collection of Components (meaning it will run last)
  2. Hook into the ImageProcessingModule_ValidatingRequest
  3. Check if the image is a GIF by interrogating the media extension (if this is the case then we terminate the process as GIFs while being seemingly supported, when I use in this fashion, they freeze up)
  4. In the case that we don’t have a GIF, we capture the current querystring of the URL
  5. Check the browser’s Request AcceptType Headers for “image/webp”, which in your browser might look something like Accept: image/webp,*/*
  6. If the check is positive i.e. not null, we know the device in question can display a WebP, so then the final stage is removing the format query string, and swapping in the WebP format query
  7. There is then a final additional step, whereby we have to move (if we have one) the quality query, to the end, so that it works, otherwise, it will be ignored if it comes before our format query! This way, you can simply append your quality preference, or apply it with GetCropUrl(cropAlias: “Background”, quality: 1) or GetCropUrl(cropAlias: “Large Background”, furtherOptions: “&quality=1”) it will all work.

It is worth noting that this for me is a work in progress, and I have some reservations about the requirement to remove the querystring before you set it. This requires more defined testing and in the meantime I will include the querystring removal in the sample code here, but I suspect you can get by without it.

using ImageProcessor.Web.Helpers;
using ImageProcessor.Web.HttpModules;
using System.Linq;
using System.Web;
using Umbraco.Core;
using Umbraco.Core.Composing;

namespace my_site_core
{
    public class SubscribeToContentServiceSavingComposer : IUserComposer
    {
        public void Compose(Composition composition)
        {
            composition.Components().Append();
        }
    }

    public class SubscribeToContentServiceSavingComponent : Umbraco.Core.Composing.IComponent
    {
        public void Initialize()
        {
            ImageProcessingModule.ValidatingRequest += ImageProcessingModule_ValidatingRequest;
        }

        private void ImageProcessingModule_ValidatingRequest(object sender, ValidatingRequestEventArgs e)
        {
            if (!e.Context.Request.Url.AbsolutePath.EndsWith(".gif"))
            {
                var queryString = HttpUtility.ParseQueryString(e.QueryString);
                if (e.Context.Request.AcceptTypes != null && e.Context.Request.AcceptTypes.Contains("image/webp"))
                {
                    queryString.Remove("format");
                    queryString["format"] = "webp";
                    if (queryString.Get("quality") != null)
                    {
                        string quality = queryString["quality"];
                        queryString.Remove("quality");
                        queryString["quality"] = quality;
                    }
                }
                e.QueryString = queryString.ToString();
            }
        }

        public void Terminate()
        {
        }
    }
}

V7

The process here is identical once we meet the image logic and onwards. However, we are using the IApplicationEventHandler to bootstrap the start-up code, and then the same ValidatingRequest action is run. This looks like this:

using ImageProcessor.Web.Helpers;
using ImageProcessor.Web.HttpModules;
using System.Linq;
using System.Web;
using Umbraco.Core;

namespace my_site_core
{
    public class WebPLoader : IApplicationEventHandler
    {

        public void OnApplicationInitialized(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext)
        {

        }

        public void OnApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext)
        {
            ImageProcessingModule.ValidatingRequest += ImageProcessingModule_ValidatingRequest;
        }

        private void ImageProcessingModule_ValidatingRequest(object sender, ValidatingRequestEventArgs e)
        {
            if (!e.Context.Request.Url.AbsolutePath.EndsWith(".gif"))
            {
                var queryString = HttpUtility.ParseQueryString(e.QueryString);
                if (e.Context.Request.AcceptTypes != null && e.Context.Request.AcceptTypes.Contains("image/webp"))
                {
                    queryString.Remove("format");
                    queryString["format"] = "webp";
                    if (queryString.Get("quality") != null)
                    {
                        string quality = queryString["quality"];
                        queryString.Remove("quality");
                        queryString["quality"] = quality;
                    }
                }
                e.QueryString = queryString.ToString();
            }
        }

        public void OnApplicationStarting(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext)
        {

        }
    }
}

Things to remember

Unfortunately, it’s not all sunshine and WebP rainbows, and there are a couple things you should keep in mind:

WebP doesn’t work on Internet Explorer. We have coded here with a safety net which ensures that IE will simply not load our new format and instead revert to it’s default behaviour, so not all is lost.

The only other quirk is that the WebP parameter is not visible in rendered source – if I inspect one of those converted photos, you will see there is no format=webp insight. Including the WebP parameter here would be how you could achieve this manually.

Do be careful with WebP sizes also, as sometimes you can end up with an image that may be larger than your original jpeg, so you might consider programmatically setting a default quality of 80 if the original is indeed a jpeg!

Usage with a CDN

If your site is behind a CDN, it likely won’t pay any attention to the request accept headers (unless you can configure that on your CDN), so the result would be that you could end up with broken images on non-supported browsers as they’d be served the webp version! A quick-fix that Darren Ferguson at Moriyama highlighted to me could be as follows

CDN Fix for WebP Umbraco

This will make unsupported browsers redirect back to the same image with an explicit webp=false in the URL so that the CDN gets a distinct URL for webp and on-webp versions.

To check that all is functioning as expected is to open your developer tools, head to network, and refresh, then watch the resources load in (where possible) as WebP – see sample log below. As expected, the GIFs are ignored!

Firefox Developer Console Network tab.

Thanks for reading!

This technique was brought to my attention by Matthew Wise so a big shout out to him! I was also helped by the community to adapt it for Version 8, and I hope you find something useful here!