How to load responsive images in AVIF and WebP using the HTML <picture> element

Illustrated graphic representing how to load AVIF and WEBP images using the HTML -picture- element
November 29, 2021


We released support for the new AVIF image format on the Contentful Images API at our Fast Forward 2021 conference. This means you can return your images stored in Contentful in the new AVIF format via the REST API with a query parameter, or via the GraphQL API with a transform function in your query.

But what is AVIF — and should you be using it right now?

Now this stuff is tricky, so strap in and get ready for a wild ride! In this post, we’ll cover:

  • Image formats, compression and browser support

  • Why image compression is important

  • The HTML element

  • Lazy loading and browser support

  • Performance improvements gained with AVIF

Are you ready? Let’s go.

What is AVIF?

In short, AVIF is the new kid on the block when it comes to image compression. Released in 2019 by the Alliance for Open Media, AVIF claims to offer 30% better compression than current alternatives such as WebP. It’s optimized for the web, designed with a low computation footprint, and what’s more — it’s open source.

Why is image compression important?

Websites in 2021 are an incredibly visual experience, full of images, animations and full-page photography. If you’re not careful, you can end up forcing visitors to your website to download hundreds of megabytes of images on a visit to your page. This risks making your website slow, inevitably causing your visitors to bounce. And we don’t want that!

A fast experience on the web is crucial to providing a great user experience for visitors to your website, and as of June 2021, Google uses Core Web Vitals scores to rank websites in search results. Core Web Vitals are currently scored on three aspects of user experience — loading, interactivity and visual stability.

1. Loading

Loading performance is measured by the Largest Contentful Paint (LCP). (This has nothing to do with the Contentful CMS in this context! 🙈 ). If you’re forcing users to download megabytes of images when the page first starts to load — for example, large hero images at the top of the page — your LCP time will increase. To provide a good user experience, the LCP should happen within 2.5 seconds of when the page first starts loading.

2. Interactivity

Interactivity is measured by the First Input Delay (FID) — and measures how soon your web application responds to user input such as clicking and typing into form fields. To provide a good user experience, pages should have an FID of 100 milliseconds or less.

3. Visual stability

Visual stability is measured by Cumulative Layout Shift (CLS). Have you ever clicked on a part of a web page, only to find that you unexpectedly clicked on something else after a rogue element or image was finally loaded? CLS is where content pops into view once it has loaded, often pushing content down or sideways on the web page — and can be extremely frustrating! CLS makes your web page unstable — and usually, large images that take time to load are to blame. A good user experience maintains a CLS score of 0.1 or less.

4. The need for speed 

Did you know that the average internet speed across the world in 2021 is only 55.13Mbs? That’s only marginally faster than the speed of a slow 3G connection as simulated in Chromium Dev Tools. If you’re the nerdy type and want to look at the source code for the throttling simulations — check out this link on GitHub!

Screenshot showing average internet speed

In “Using Modern Image Formats: AVIF And WebP”, Addy Osmani, engineering manager at Google Chrome, gives us this fascinating piece of data: 

“If you’re optimizing for the Web Vitals, you might be interested to hear that images account for ~42% of the Largest Contentful Paint element for websites.”

If almost 50% of your LCP is down to images — you need to deliver them to your website visitors in as few bytes as possible. The smaller the image file sizes are on your web pages, the less time it takes for visitors to your websites to download them — and this is especially important on slow internet connections and older, slower devices. 

Minimizing the size of your images ensures your Core Web Vitals scores are as good as possible across the board, which means you provide a better experience for your users, and ultimately means your pages rank better in Google search results. And to minimise the size of your images — you need to use the image format that yields the lightest results.

So — if AVIF images currently offer the smallest image file sizes, we should all get on the AVIF train ASAP, right?

Should you convert all your images to AVIF right now?

Whilst AVIF offers better compression and smaller resulting file sizes than WebP, there are some downsides to adopting this new format in 2021.

AVIF may not be able to compress non-photographic images as well as PNG or lossless WebP. I found this to be the case in my initial experiments with the hero image on the homepage of my personal website

Screenshot of Salma's stream

Whilst this image does contain photographic imagery of my face, most of the image is text and illustrations. As a WebP image, this image came in at 118kb, whilst as an AVIF image, the image size increased to 125kb. 

Secondly, at the time of writing this article, not all browsers currently support the AVIF image format. Edge, Safari and some mobile browsers are not quite there yet.

Screenshot of AVIF caniuse

But all is not lost! 

There is a beautiful way we can harness the power of native HTML to serve different supported image formats to browsers using the HTML <picture> tag. And what’s more, when other browsers do catch up with the AVIF train — you won’t need to change any code! 

Before we look at the HTML, let’s take a look at how you can convert your images stored in Contentful to the new AVIF format using the REST and GraphQL APIs.

How to convert your images to AVIF using the Contentful Images API

The Contentful Images API offers a variety of image transformations and manipulations as query parameters on the URL pointing to an image asset. You can change the quality of an image, resize it, add a background color, crop it, add rounded corners and more, as well as change the format of an image. 

Option 1: Using query parameters on an image URL

To convert your images stored in Contentful to AVIF, add fm=avif as a query parameter to your image URL. We’ll be using this technique later in the HTML example code.

Here’s an example URL for you to play with in your browser. Experiment with changing the format parameter to jpg, png, webp, gif or avif and compare the size of the image returned in the network tab!

For the impatient among you, here’s how the image formats compare in terms of size:

  • jpg: 76.8kb

  • png: 125kb

  • webp: 167kb

  • gif: 1.2MB

  • avif: 25.3kb

Screenshot of the size and image type of the different variants for the cat heavy breathing intensifies

AVIF is the clear winner in terms of reduction in size for this image. However, one interesting thing to note is that whereas the original animated GIF comes in at a whopping 1.2MB, converting the image to WebP reduces the image size by a massive 87% and preserves the animation frames! I think that’s a fascinating achievement and a wonderful nugget of information! I was so excited at this discovery that I shared it on Twitter!

Option 2: Using GraphQL

If you’re using GraphQL, you can convert your images to AVIF via a transform function on the image URL in the GraphQL query, like so:

And here’s the response returned. You’ll notice that this is the same image URL we used in the example above, demonstrating that the GraphQL API communicates directly with Contentful’s Images API to return what you need in your GraphQL response.

However — while you can retrieve images in AVIF format in GraphQL — remember that not all browsers support this image format yet — so you don’t want to lock yourself down to the AVIF image format. 

Now let’s look at how we can take a base image URL and serve it in different ways to browsers that support different image formats using Contentful’s Images API and the native HTML <picture> tag. 

And here is what you’ve all been waiting for!

How to serve responsive images with AVIF in supported browsers using the HTML <picture> tag

I’ve attempted to understand the low-down on responsive images from MDN for many years and I must admit it was hard to grasp! This post from Stefan Judis provided a great help in getting to grips with this, in conjunction with MDN.

One does not simply use wirth:100% to create responsive images meme

Before we get into the code, we need to understand what we mean by responsive images. Responsive images are not simply about giving all images width: 100% to fill the size of a container! The real power in responsive images is serving different image files of different sizes to different viewport sizes — and what’s more — screens with different pixel densities or display resolutions.

This is called Resolution Switching, and by providing browsers with an array of image URL options by configuring a <source> element’s srcset, media, and type attributes, the most compatible image — according to the current layout and display capabilities — will be displayed. 

Let’s get straight into the HTML code example, and unpack what it does.

HTML <picture> element

The <picture> HTML element wraps <source> elements and one <img> element to offer alternative versions of an image for different displays, devices or browsers (depending on the image formats supported).

The magic in the <picture> element is that the browser chooses the most appropriate <source> element to display. If no matches are found — or the browser doesn't support the <picture> element — the browser falls back to the URL in the src attribute of the <img> element. The selected image is then presented in the space occupied by the <img> element. To prevent Cumulative Layout Shift as the image is loading, it’s important to add the height and width attributes to the <img> element for this fallback.

HTML <source> element

The HTML <source> element provides multiple media types for the <picture> element to select the best-fit for the browser or device. You can also use the <source> element inside HTML <audio> or <video> elements in the same way.

In this example, the <source> elements are offering image formats in type="image/avif" and type="image/webp", which browsers will choose to display in that order depending on support.

HTML srcset and sizes attributes

The HTML srcset attribute is a comma-separated list of strings of image URLs and either a width descriptor — such as 300w, or a pixel density descriptor — such as 1.5x

The HTML sizes attribute is a comma-separated list of media conditions paired with sizes to describe the final rendered image width — in CSS pixels, not physical pixels (read more about different types of pixels on MDN). The browser wants to know the best-fit resources to request as the page is loading. The sizes attribute helps the browser to calculate the layout of the page and request the most suitable images for the layout — even before the CSS has loaded.

The srcset and sizes attributes work together in the browser to determine which image defined in the srcset to request as the page is being rendered or the viewport resized.

In this code example, four image URLs are provided in the srcset for four different image  widths: 500w, 900w, 1300w, 1700w. Notice that the w={width} parameter on the image URLs is resizing the base image to match with size of the width descriptor using the capabilities of the Contentful Images API. When not using the Contentful Images API, you may wish to choose completely different image URLs at different viewport widths to serve different image sizes. 

A note on the magic 736 number

For blog posts on my personal website, the maximum width of the image container will only ever be 736px — in CSS pixels.

The sizes attribute states that under a viewport width of 736px, choose the most appropriate image from the srcset, considering that the image will be laid out across the entire viewport width (100vw). Depending on the device, full viewport width (100vw) could translate to 300 device pixels at 1 DPR, 600 device pixels at 2 DPR and 900 device pixels at 3DPR.

At a viewport width of 736px and above (remember, it’s 736 CSS pixels), the browser knows that the image widths will not exceed 736 CSS pixels as the CSS is controlling the container width. Depending on the device, 736 CSS pixels will be 1472px at 2DPR and 2208px at 3DPR. At all viewport widths, the browser will choose the closest match from the srcset list. Like magic!

This is an entirely personal example, and you should configure your image widths according to the layout of your web pages. You can also make further optimizations using CSS calc() to account for the padding values inside the container to load even smaller images at different viewport widths.

It’s also important to note that the srcset and sizes attributes only come into effect when the <source> element is a direct child of a <picture> element.

HTML loading=”lazy” and decoding=”async”

Lazy loading is a strategy to identify resources such as images, video and other media as non-blocking, or non-critical — and to load these only when needed, such as when a user scrolls that resource into view.

To prevent visitors to web pages downloading a full page of images that aren’t visible in the viewport yet, we can use native HTML lazy loading with the loading="lazy" attribute to defer image loading until a user scrolls near it — no JavaScript required! As stated before, to prevent Cumulative Layout Shift, be sure to add the height and width of the base image as attributes to the <img> element, which will instruct the browser to leave the correct space on the page in which to finally load your image when it scrolls into view.

And in addition, the decoding="async" attribute tells the browser that the image loading can be performed asynchronously, so that content below the resource — such as paragraphs of text — are not blocked by the image taking a moment to load.

It’s worth a mention that loading="lazy" isn’t entirely supported across all browsers yet, but at the time of writing this article, Safari just shipped it in the newest Tech Preview.

Screenshot of loading caniuse

The code in action

Here’s a video of the code example in action at 1 DPR. Notice the lazy-loading in action as the page is scrolled, and how the browser loads different image URLs from the srcset when the browser is resized! And let me reiterate — no JavaScript required!

Screenshot of responsive avif

How did serving AVIF improve my website?

The code example above is a lot of HTML to write! But I like it for how self-documenting it is. And what’s more, I’ve bagged some significant performance improvements for my personal website while learning all about AVIF.

For image-heavy blog posts, and in particular this blog post, converting all images from WebP to AVIF reduced the cumulative size of images on a desktop screen at 2 DPR from just over 1Mb to just 404kb. And on mobile devices at 1 DPR, it reduced the cumulative sizes of images from 430kb to 124kb. That’s a 60-70% reduction across device sizes! Those are some huge savings! And plus, all images are lazy-loaded, which means the browser doesn’t request them until the image is near to the visible viewport.

The bottom line

Responsive images on the web are a tricky business to get right! But if you get them right, you provide a better user experience for your website visitors across browsers, devices and screen sizes, and you get bonus points from Google in your Core Web Vitals scores.

If you’d like to see the final code example as a React component in the code for my personal blog site, check it out on GitHub. And if you have any questions about responsive images and how you can make the best of the Contentful Images API, find me on Twitter!

Start building

Use your favorite tech stack, language, and framework of your choice.

About the author

Don't miss the latest

Get updates in your inbox
Discover new insights from the Contentful developer community each month.
add-circle arrow-right remove style-two-pin-marker subtract-circle remove