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.