A toy spiderman sat on a laptop

Tutorial

How to Optimise Banner Images for Web Performance

A common pattern in website layouts is a hero image or banner. This is an image, at the top of the page that spans the full-width of the screen. There are several factors that combine to make these challenging to deliver in a performant way.

  • Because they are large images appearing above the fold, they can have a significant impact on performance, especially with the introduction of Core Web Vitals (CWV).
  • Often, due to aspect ratio, we want to 'art-direct' the image so that we have a different aspect ratio on mobile.

In this article I will detail an approach to create a hero banner that achieves the following:

  •  Alternative aspect ratios for mobile and desktop
  •  Delivers appropriate resolution images for retina and non-retina screens
  •  Delivers the best supported format of image supported by the browser
  •  Avoids layout shift to meet CWV requirements.

Our starting point is a simple image tag using our slim banner image at a medium size, and medium quality. This will act as a reasonable fallback image for any browsers that don't support our enhancements.

<div>

   <img src="images/banner-slim-1230x246-q55.jpg" alt="" width="1230" height="245" />

</div>

N.B. In this article all image files will be named using the file size (1230x246) and quality (q55) to help explain what is happening.

An important thing to note is the addition of the width and height attributes, these are crucial in order to avoid layout shift (a key metric in Chrome Web Vitals). Adding these attributes lets the browser know the aspect-ratio of the image and it can then allocate the correct amount of space to the image before it has been loaded, in order to prevent page content moving around.

As we have different aspect ratio images (which could even be different images entirely if we wanted) we will need to use a picture element with different sources.

<div>
    <picture>
        <source media="(max-width: 768px)"
                srcset="images/banner-tall-1200x400-q55.jpg w1200"
                sizes="100vw" />

        <source media="(min-width: 769px)"
                srcset="images/banner-slim-2560x512-q55.jpg w2560"
                sizes="100vw" />

        <img src="images/banner-slim-1230x246-q55.jpg"
             alt=""
             width="1230"
             height="245" />
    </picture>
</div>

The browser will now choose the appropriate source for the image based on the media queries provided. Note that it will use the first `<source>` with a matching media query, so the order of the elements is important.

However, there is now a problem with the above markup. Above, we added `width` and `height` attributes to our image to aid with Cumulative Layout Shift. Now that we have 2 different sizes of image we need to provide different width and height attributes for each source.

This is actually a feature still under development in broswer engines currently it has only shipped in Chrome (v90), will hopefully ship soon in Webkit, and it is still an open bug for Firefox. Given that the Chrome Web Vitals metric will be assessed using the Chrome rendering engine we can gain the CLS benefit of adding this to our markup, and Chrome users will avoid any layout shifting as the image loads. As the feature lands in other browsers, they will automatically pick up the enhancement 👍.

<div>
    <picture>
        <source media="(max-width: 768px)"
                srcset="images/banner-tall-1200x400-q55.jpg w1200"
                sizes="100vw"
                width="1200"
                height="400" />

        <source media="(min-width: 769px)"
                srcset="images/banner-slim-2560x512-q55.jpg w2560"
                sizes="100vw"
                width="2560"
                height="512" />

        <img src="images/banner-slim-1230x246-q55.jpg"
             alt=""
             width="1230"
             height="245" />
    </picture>
</div>


Let's review our progress so far:

  • ✅ Alternative aspect ratios for mobile and desktop
  • ⬜️ Delivers appropriate resolution images for retina and non-retina screens
  • ⬜️ Delivers the best supported format of image supported by the browser
  • ✅Avoids layout shift to meet CWV requirements.

Currently we are serving one image to small screens and one image to large screens. This is not very optimised solution. Some users will be downloading larger images than required, and high density screen users will likely be seeing fuzzy images.

Firstly lets provide images in a better format for those browsers that support it. Webp is an image format that has wide support and is able to deliver higher quality images at a smaller file size than jpeg.

To add webp support, we simply need to add webp sources to our `<picture>` element.

<div>
    <picture>
        <source media="(max-width: 768px)"
                type="image/webp"
                src="images/banner-tall-1200x400-q45.webp"
                width="1200"
                height="400" />

        <source media="(max-width: 768px)"
                src="images/banner-tall-1200x400-q55.jpg"
                width="1200"
                height="400" />

        <source media="(min-width: 769px)"
                type="image/webp"
                src="images/banner-slim-2560x512-q45.webp"
                width="2560"
                height="512" />

        <source media="(min-width: 769px)"
                src="images/banner-slim-2560x512-q55.jpg"
                width="2560"
                height="512" />

        <img src="images/banner-slim-1230x246-q55.jpg"
             alt=""
             width="1230"
             height="245" />
    </picture>
</div>

With the above markup, any browser that supports webp images, will serve a webp image that is smaller in file size (note that the webp image is saved out at a lower quality, but will be visibly the same quality to the user). 

Now let's consider adding support for different screen resolutions. The problem with high density screens is that serving images that are twice as big, even when fully optimised still results in large image sizes. So I'm going to utilise a technique documented by Google developer advocate Jake Archibald for optimizing images for high density screens.

Essentially this technique boils down to the fact that the images we provide to high density screens can be saved at a much lower quality because they are displayed at half of their size. We then just need to ensure that we tell the browser that these larger, lower quality images should only be used on high density screens. As if they get loaded on 1x displays users will clearly see the fact that the image quality is not good.

So here is our updated markup (brace yourself):

<div>
    <picture>
        <!-- small high-density screens get different apsect ratio image, also low quality due to density -->
        <source media="(max-width: 768px) and (-webkit-min-device-pixel-ratio: 1.5)"
                type="image/webp"
                srcset="images/banner-tall-800x266-q50.webp 800w,
                        images/banner-tall-1200x400-q45.webp 1200w,
                        images/banner-tall-1600x533-q40.webp 1600w"
                sizes="100vw"
                width="1200"
                height="400" />

        <source media="(max-width: 768px) and (-webkit-min-device-pixel-ratio: 1.5)"
                srcset="images/banner-tall-800x266-q60.jpg 800w,
                        images/banner-tall-1200x400-q55.jpg 1200w,
                        images/banner-tall-1600x533-q50.jpg 1600w"
                sizes="100vw"
                width="1200"
                height="400" />

        <!-- larger high density screens get large, low-quality images -->
        <source media="(min-width: 769px) and (-webkit-min-device-pixel-ratio: 1.5)"
                type="image/webp"
                src="images/banner-slim-1230x246-q45.webp 1230w,
                     images/banner-slim-1860x372-q40.webp 1860w,
                     images/banner-slim-2560x512-q40.webp 2560w,
                     images/banner-slim-3840x768-q40.webp 3840w"
                     sizes="100vw"
                width="2560"
                height="512" />

        <source media="(min-width: 769px) and (-webkit-min-device-pixel-ratio: 1.5)"
                src="images/banner-slim-1230x246-q55.jpg 1230w,
                     images/banner-slim-1860x372-q50.jpg 1860w,
                     images/banner-slim-2560x512-q45.jpg 2560w,
                     images/banner-slim-3840x768-q45.jpg 3840w"
                sizes="100vw"
                width="2560"
                height="512" />

        <!-- large low-density screens get large, but higher quality images, as they don't benefit form high density -->

        <source media="(min-width: 769px)"
                type="image/webp"
                src="images/banner-slim-930x186-q75.webp 930w,
                     images/banner-slim-1230x246-q75.webp 1230w,
                     images/banner-slim-1860x372-q75.webp 1860w,
                     images/banner-slim-2560x512-q75.webp 2560w"
                sizes="100vw"
                width="2560"
                height="512" />

        <source media="(min-width: 769px)"
                src="images/banner-slim-930x186-q75.jpg 930w,
                     images/banner-slim-1230x246-q70.jpg 1230w,
                     images/banner-slim-1860x372-q70.jpg 1860w,
                     images/banner-slim-2560x512-q65.jpg 2560w"
                     sizes="100vw"
                width="2560"
                height="512" />


        <!-- fallback to a medium-ish size and quality image -->
        <img src="images/banner-slim-1230-246-q55.jpg"
             alt=""
             width="1230"
             height="245" />
    </picture>
</div>

Wow. That's a lot! Let's break down what's going on a bit. For each source we've switched from one source to a srcset of different sized images. The browser will then use the most appropriate one based on the current screen size. We are providing images for three different situations - small high-density screens, large high-density screens, and large standard density screens. For each of these we provide tailored images to suit the context - where high density screens can be provided a larger lower quality image. You'll notice that I've not included small standard density screens, this is because the vast majority of screens below 768px will be high density mobile screens, and any that aren't will default to the `<img>` tag, which is an acceptable fallback.

Now, I'll accept that this is a large amount of markup for one image. Would I recommend doing this for every image? Absolutely not! But for an above-the-fold, large hero image, investing the time to deliver it well will pay off in performance - resulting in good CWV scores and even more importantly your user will see your content much quicker, and won't be left waiting on large images, or be forced to download unnecessarily large images for their device size.

Notes

The image sizes you use will be very dependent on your context, and the image qualities you can save the image out at will be dependent on your image. 

It's also worth noting that I wouldn't expect all of these images to be manually saved out. I would only recommend using this technique where you can generate all the required image sizes automatically. Either in your CMS on upload, or by using an image service in your frontend such as Glide or imgx.

Got an upcoming project?

Let's talk. Call us on 0330 2233 182 or contact us.