Close menu

Optimising Image Delivery

Images are crucial for websites, but they can be the largest files that are presented on them, which of course means they contribute greatly to poor loading speeds.

As technology shifted from using low to high resolution displays, the images we use have doubled and tripled in size! Making page load speeds even worse!

For years now, web technologies have added features to help mitigate this drastic increase in asset sizes, but getting it right can be tricky - this article hopes to demystify the arguably confusing syntax of responsive images, as well as share some techniques to squeeze even more performance out of them. These solutions are modern, and so as you might expect, won't work for older browsers such as IE.

Responsive images

In a responsive design world, we need to ensure the right size images are being loaded for the target devices. HTML has built in support for doing this.

Resolution switching

In addition to the src attribute, the <img> element has 2 attributes to aid in loading images at the right resolution - srcset and sizes.

The srcset Attribute

srcset takes a comma separated list of image sources (just like the src attribute) and either their intrinsic widths or a pixel density descriptor:

<img src="foo.jpg" srcset="foo-320px.jpg 320w, foo-640px.jpg 640w">

<img src="foo.jpg" srcset="foo-320px.jpg 1x, foo-640px.jpg 2x">

Notice the format includes a space followed by either the pixel density descriptor as a multiplier or the intrinsic width (i.e. the actual width in pixels of the image file) but specified with a w denoting width.

you can't mix widths and pixel densities in the srcset.

The browser will determine which of these images is best, based on a number of factors, including network conditions, and load only that one - the src attribute then serves as the 'fallback' for browsers that don't support srcset.

For the purposes of loading the right size images without art direction, we often find that specifying pixel densities can be more helpful than width descriptors. This is because in those cases where we're expecting the image usage in size, aspect ratio, composition etc to be exactly the same, and only differ in the type of screen (such as HiDPI/retina vs standard screens) we can specify only the srcset without having to provide hints using the sizes attribute.

Using width descriptors gives you better control over art direction. They inform the browser of the different widths of the image assets available for use as that embedded image, but it's still up to the browser to determine which is optimal to use - this is where the sizes attribute becomes useful.

The sizes Attribute

This optional attribute allows you to give the browser some hints about which images to use in which cases. It's only used if your srcset specifies width descriptors.

It takes a similar format to the srcset - except that you specify media queries and an image's intended display width:

<img
    src="foo.jpg"
    srcset="foo-320px.jpg 320w, foo-640px.jpg 640w"
    sizes="(min-width: 1024px) 640px, 320px"
>

This means that for viewports of 1024px and above, this image will be displayed at 640px.

The final default value doesn't get any media query.

Now the browser can use these media-queries and the display widths you provided to decide which image to use, by determining which image in the srcset best matches the size you want to display it at, for the viewports you've specified in the sizes.

Even though sizes give you more granular control over which images are used, it's important to remember that it's ultimately decided by the browser, and so really only serves as a 'suggestion'.

To get it to work with more confidence, you have to match your srcset images to the display widths you are using in your sizes - so it's great for art direction where you have the same image but with different composition or aspect ratios on different viewports.

If you need even more control on the art direction of images, then the <picture> element may be better suited.

The <picture> Element

This element takes multiple <source> elements and one <img> element as children.

The <img> element is used to present the image itself, but the <source> elements specify a number of potential candidates for it.

<source> also uses the srcset attribute, just like the <img> element, to provide a list of possible image sources to use. Alongside srcset, the element can take a media attribute, which specifies a media condition for the viewport (essentially a css media query to match):


<picture>
    <source
        srcset="foo-1024px.jpg"
        media="(min-width: 1280px)"
    >
    <source
        srcset="foo-640px.jpg"
        media="(min-width: 1024px)"
    >
    <img src="foo.jpg">
</picture>

If a source's media condition does not evaluate to true, it's skipped and the next source is evaluated, and so on.

In addition to the srcset and media attributes a <source> element can take a type attribute, which specifies the image mime type of the referenced image. If a browser does not support a specified mime type, that source is skipped and the next one evaluated.


<picture>
    <source
        srcset="foo-1024px.webp"
        type="image/webp"
    >
    <source
        srcset="foo-1024px.jpg"
        type="image/jpeg"
    >
    <img src="foo.jpg">
</picture>

The type checking feature means the <picture> element can be used to progressively use newer image formats.

Performance Concerns

Now we have appropriate images loading for our devices, we might think we're done! But actually, we have introduced a pretty tricky performance issue.

Let's say we're on a HiDPI screen, straight away our images are double the resolution - and therefore larger in file size, which in turn means slower to load. Because these are embedded images, they will delay the window load event.

Now what if those images are somewhere toward the bottom of the page, near the footer and way below the fold? That's slowing the page down for images that are not being seen, and may never actually be seen.

The answer to this is lazy loading; a technique that loads images only after the load event has fired and when the image is in (or is about to come into) the viewport.

Native lazy loading

HTML actually has this built in! but being an experimental feature it's inconsistently supported (at time of writing, it is not supported by Safari and IE). In addition, it's not configurable and our testing has yielded some mixed results across different browsers and situations, such as loading images that are in our opinion, too far below the fold to merit loading.

As an anti-tracking measure, this feature only works when JavaScript is enabled.


<img loading="lazy">

You'd usually want to set this on images that are below the fold, or on images that are very large in file size.

Scripted Lazy Loading Solution

Implementing lazy loading with JavaScript not only means a solution that can work for browsers that do not yet implement the native loading attribute, but also means more control of when images are loaded. Of course you'll need to manually implement this to suit your needs.

Placeholder Images

In terms of performance, network requests are expensive - so you'd want to reduce the number of requests being made to ensure faster page loads. For this reason, features such as srcset ensure that only a single image is requested; either the src or one from the srcset.

While this saves on network requests, image file sizes can vary greatly, so in some cases, the single image that is requested can take a very long time to load, and in doing so, delay the load event from firing.

Our experience has taught us that, in some cases, the perception of speed is more valuable to a user than speed itself.

One technique to make image heavy pages appear to load faster than they actually do is to use smaller, lower quality placeholder images that will download quickly, and a scripted lazy loading solution to get the correct size image, after the load event is fired. Generally, this is discouraged for raw performance, and when done above the fold, may negatively impact Core Web Vitals measurements. However we maintain that the technique results in better UX, because it means that the page will actually reach the loaded state faster. Furthermore, even if the correct quality image takes a while to download, an image is still displayed which can be made out by a user, albeit in terrible quality - it will eventually be replaced with the larger, high quality file.

The only way placeholder images can be used, is if you're using a scripted lazy loading solution; the native solution does not allow you to do this because, as mentioned, it is not configurable.

Another reason for using placeholders for a scripted solution is that the alternative is leaving the src attribute empty, which is invalid HTML! And we certainly don't want to be writing invalid markup.

So what does a scripted solution look like?

Here's a possible solution in ES6 powered by our ScriptuccinoJS utilities as an example...


import { elementComesIntoViewport } from '@beyondthesketch/scriptuccinojs';

elementComesIntoViewport(
    document.querySelectorAll('img[data-srcset]'),
    (img, observer) => {
        if (!img.hasAttribute('srcset')) {
            img.setAttribute(
                'srcset',
                img.getAttribute('data-srcset')
            );
            img.removeAttribute('data-srcset');
            observer.unobserve(img);
        }
    }
);

The above solution is designed for modern browsers that support srcset. For all images you want lazy loaded, simply set the src attribute as usual to the url of the image at its smallest resolution and file size - this image will always be loaded, so the key is to keep them as small as possible. Then use a data-srcset attribute instead of a srcset. The code will feature sense the correct support and change the data-srcset attribute to a srcset, triggering the browser to load the optimal image.

Decoding

You can hint to the browser if your image should be decoded synchronously or asynchronously.


<img decoding="async">

By default the browser chooses, but async tells the browser that the image should not stop it from presenting other content while the image is being rendered.

We'd suggest all images be decoded async. Use cases for sync might be for image heavy SPAs, or for your main above the fold hero image, so that the whole block is rendered together at once (atomic presentation) - but remember, this means more time with a blank page! We'd generally advise against that.

Progressive JPEG

A simple one - when using JPEG images, make sure they are progressive. While these files will be a little larger than baseline JPEGs, the browser can render lower resolution views of the image as it is being loaded/decoded, making for a better user experience in most cases. It's particularly useful to use them with async decoding.

When using together with a scripted lazy loading solution, placeholder images also become unecessary.

Picking The Right Format

For photographs and images with gradation and many colours and tones, JPEG offers the best quality and compression. Where your images are more of an illustration with bigger blocks of solid colours in a limited colour pallette then an 8bit PNG or a GIF is best, if you need alpha transparency, then a 24bit PNG is the one to go for - while 8bit PNGs and GIFs also offer transparency, they apply matting which results in a rough, coloured edge around the image.

Next Generation Image Formats

We're all used to jpg, gif and png, but there are some other, newer formats that can be considered.

Jpeg2000 is an upgrade of the standard, offering better quality at smaller file sizes, but is only supported in Safari.

The new kid on the block is the WebP format, developed by Google. This new format provides much better compression of images while maintaining very good detail. They also offer transparency just like a 24-bit PNG. If WebP is an option for you, there's very little reason not to use it - combine with the picture element, to provide fallbacks for the unsupported browsers.

Bring It All Together

Not all these techniques and tips work for every use case, there is no one size fits all, combine the ones that fit your scenarios and keep measuring to ensure your image delivery remains optimal as you update and grow your website or WebApp.

First published: 12/09/2022