In this post I'll outline 8 image loading optimization techniques to minimize both the bandwidth used for loading images on the web and the CPU usage for image display. I'll present them in the form of an annotated HTML example to make it easy for folks to reproduce the results. Some of these techniques are more established, while others are somewhat novel. Ideally, your favorite mechanism for publishing web documents (like a CMS, static site generator, or web application framework) implements all of these out-of-the-box. I'll keep a list updated at the end of this posts with technologies that provide all of the optimizations outlined here.
Together the techniques optimize all elements of Google's Core Web Vitals by
- Minimizing the Largest Contentful Paint (LCP) through reducing bytes, caching, and lazy loading.
- Keeping Cumulative Layout Shift (CLS) to zero.
- Reducing First Input Delay(FID) through reduced (main-thread) CPU usage.
View the source of this sample image to see all the techniques in action:
Optimization techniques #
Responsive layout #
This is a well understood technique to make an image use the available horizontal space up until its maximum size while retaining the aspect ratio. New in 2020 is that web browsers will reserve the correct vertical space for the image before it loads if the width
and height
attributes are provided for the img
element. This avoids Cumulative Layout Shift (CLS).
<style>
img {
max-width: 100%;
height: auto;
}
</style>
<!-- Providing width and height is more important than ever. -->
<img height="853" width="1280" … />
Lazy rendering #
The second technique is more cutting edge. The new CSS attribute content-visibility: auto
instructs the browser to not bother layouting the image until it gets near the screen. This has all kinds of benefits, but the most important one might be that the browser will not bother decoding our blurry placeholder image or the image itself unless it has to, saving CPU.
Update 01/27/2021 contain-intrinsic-size no longer needed #
An earlier version of this post explained how to use contain-intrinsic-size
to avoid CLS impact of content-visibility: auto
but as of Chromium 88 this is no longer needed for images that provide width
and height
as explained above. Other browser engines do not yet (01/27/2021) implement content-visibility: auto
and would presumably follow Chromium's lead on this special case. So, yaihh, this is much simpler now!
<style>
/* This probably only makes sense for images within the main scrollable area of your page. */
main img {
/* Only render when in viewport */
content-visibility: auto;
}
</style>
AVIF #
AVIF is the most recent image format that has gained adoption in web browsers. It is currently supported in Chromium browsers, and available behind a flag in Firefox. Safari support isn't available yet, but given that Apple is a member of the group that is behind the format, we can expect future support.
AVIF is notable because it very consistently outperforms JPEG in a very significant way. This is different from WebP which doesn't always produce smaller images than JPEG and may actually be a net-loss due to lack of support for progressive loading.
For more info on AVIF encoding and quality settings, check out my dedicated blog post.
To implement progressive enhancement for AVIF, use the picture
element.
The actual img
element is nested in the picture
. This can be quite confusing, because the img
is sometimes described as fallback for browsers without picture support but basically the picture
element only helps with src
selection but has no layout itself. The element that is drawn (and which you style) is the img
element.
Until very recently it was relatively difficult to actually encode AVIF images on the server-side, but with the latest version of libraries like sharp it is now trivial.
<picture>
<source
sizes="(max-width: 608px) 100vw, 608px"
srcset="
/img/Z1s3TKV-1920w.avif 1920w,
/img/Z1s3TKV-1280w.avif 1280w,
/img/Z1s3TKV-640w.avif 640w,
/img/Z1s3TKV-320w.avif 320w
"
type="image/avif"
/>
<!-- snip lots of other stuff -->
<img />
</picture>
Load the right number of pixels #
You might have noticed the srcset
and sizes
attributes in the snippet above. Using the w
selector it tells the browser which URL to use based on the physical pixels that would be used if the image was drawn to the user's device given the width calculated from the sizes
attribute (which is a media query expression).
With this the browser will always download the smallest possible image that provides the best image quality for the user. Or it may select a smaller image if, for example, the user has opted into some kind of data-saving mode.
Fallbacks #
Provide more source elements with srcset
s for browsers that only support legacy image formats.
<source
sizes="(max-width: 608px) 100vw, 608px"
srcset="
/img/Z1s3TKV-1920w.webp 1920w,
/img/Z1s3TKV-1280w.webp 1280w,
/img/Z1s3TKV-640w.webp 640w,
/img/Z1s3TKV-320w.webp 320w
"
type="image/webp"
/>
<source
sizes="(max-width: 608px) 100vw, 608px"
srcset="
/img/Z1s3TKV-1920w.jpg 1920w,
/img/Z1s3TKV-1280w.jpg 1280w,
/img/Z1s3TKV-640w.jpg 640w,
/img/Z1s3TKV-320w.jpg 320w
"
type="image/jpeg"
/>
Caching / Immutable URLs #
Embed a hash of the bytes in the image in the URL of the image. In the examples above I'm doing that with the Z1s3TKV
in the image URLs. That way the URL will change if the image changes and respectively you can apply infinite cache expiration for your images. You want your caching headers to look something like this cache-control: public,max-age=31536000,immutable
.
immutable
is the semantically correct cache-control
value, but unfortunately it isn't widely supported in browsers (I'm looking at you, Chrome). max-age=31536000
is the fallback to cache for a year. public
is important to allow your CDN to cache the image and deliver it from the edge. But only use that if it is appropriate from a privacy perspective.
Lazy loading #
Adding loading="lazy"
to the img
instructs the browser to only start fetching the image as it gets closer to the screen and is likely to actually be rendered.
<img loading="lazy" … />
Asynchronous decoding #
Adding decoding="async"
to the img
gives the browser permission to decode the image off the main thread avoiding user impact of the CPU-time used to decode the image. This should have no discernible downside except that it cannot always be the default for legacy reasons.
<img decoding="async" … />
Blurry placeholder #
A blurry placeholder is an inline image that provides the user some notion of the image that will load eventually without requiring fetching bytes from the network.
Some notes on the implementation provided here:
- It inlines the blurry placeholder as a
background-image
of the image. This avoids using a second HTML element and it naturally hides the placeholder when the image loads, so that no JavaScript is needed to implement this. - It wraps the data URI of the actual image in a data URI of a SVG image. That is done because the blurring of the image is done at the SVG level instead of through a CSS filter. The result is that the blurring is only performed once per image when the SVG is rasterized, instead of on every layout saving CPU.
<img
style="
…
background-size: cover;
background-image:
url('data:image/svg+xml;charset=utf-8,%3Csvg xmlns=\'http%3A//www.w3.org/2000/svg\'
xmlns%3Axlink=\'http%3A//www.w3.org/1999/xlink\' viewBox=\'0 0 1280 853\'%3E%3Cfilter id=\'b\' color-interpolation-filters=\'sRGB\'%3E%3CfeGaussianBlur stdDeviation=\'.5\'%3E%3C/feGaussianBlur%3E%3CfeComponentTransfer%3E%3CfeFuncA type=\'discrete\' tableValues=\'1 1\'%3E%3C/feFuncA%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Cimage filter=\'url(%23b)\' x=\'0\' y=\'0\' height=\'100%25\' width=\'100%25\'
xlink%3Ahref=\'data%3Aimage/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAGCAIAAACepSOSAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAs0lEQVQI1wGoAFf/AImSoJSer5yjs52ktp2luJuluKOpuJefsoCNowB+kKaOm66grL+krsCnsMGrt8m1u8mzt8OVoLIAhJqzjZ2tnLLLnLHJp7fNmpyjqbPCqLrRjqO7AIeUn5ultaWtt56msaSnroZyY4mBgLq7wY6TmwCRfk2Pf1uzm2WulV+xmV6rmGyQfFm3nWSBcEIAfm46jX1FkH5Djn5AmodGo49MopBLlIRBfG8yj/dfjF5frTUAAAAASUVORK5CYII=\'%3E%3C/image%3E%3C/svg%3E');
"
…
/>
(Optional-ish) JavaScript optimization #
Browsers may feel obliged to rasterize the blurry placeholder even if the image is already loaded. By removing it on image load, we solve that problem. Also, if your images contain transparency, then this is actually not optional as otherwise the placeholder would shine through.
<script>
document.body.addEventListener(
"load",
(e) => {
if (e.target.tagName != "IMG") {
return;
}
// Remove the blurry placeholder.
e.target.style.backgroundImage = "none";
},
/* capture */ true
);
</script>
Tools #
This is a list of known technologies and tools implementing all of these optimizations:
If you know of a technology (can be a combination of multiple "modules" or similar if they work well together) that should be on this list, please ping me.
Published