React srcset for Responsive Images
If you read about the srcset attribute on images previously but never got around to implementing it, keep reading. Your users with their limited phone data packages will love you!
Images are essential on the web, and together with videos, they make up for the most bytes downloaded on most websites. To illustrate: here is how much of the downloaded bytes are images for a few popular sites in 2022:
telegraph.co.uk: 36%
youtube.com: 25%
amazon.co.uk: 78%
netflix.com: 49%
stackoverflow.com: 25%
Since you most likely have a lot of images on your site, two crucial questions need to be answered:
- How do I ensure high-quality images for my users?
- How do I ensure that they don’t download unnecessary big images?
You cannot just serve huge images to everyone. For example, it will affect performance immensely for users on a medium/slow connection. Those users will most likely have significantly spent less time on your page, and the conversion rate will be lower if you run an ecommerce.
What’s the solution?
Historically, there have been many attempts to solve this. Most of these attempts have been with Javascript. The correct image to be served is determined by checking the device screen width and the device pixel ratio after the page is loaded. This approach allows you to make a fair assumption on which image to serve. There are, however, two significant problems with this:
- The calculation happens after the page is loaded. What would you show before that?
- What happens when the browser landscape changes and your algorithm is out of date? And how do you know when it is outdated?
A while ago, the srcset and sizes attributes were introduced on <img> tags. These powerful attributes enable the browser to determine which image to serve the user! No javascript, no waiting for the page to load.
With the srcset attribute, we can define different sources and their widths for an image, and with the sizes attribute, we can hint to the browser which source to use based on the browser viewport width.
It can be implemented in React like this:
function ResponsiveImage() {
return (
<img
src="image-url-1280.jpg"
srcSet="image-url-300.jpg 300w, image-url-768.jpg 768w, image-url-1280.jpg 1280w"
sizes="(max-width: 300px) 300px, (max-width: 768px) 768px, 1280px"
/>
);
}
The srcset attribute accepts a pair of image URLs and width descriptors separated by space. We can provide multiple sources separated by commas. The sizes attribute accepts a pair comprised of a media query and the source's size. Here we can again pass multiple sizes separated by a comma where the last one is the default size source.
In our example above, when the browser viewport width is less than 300px, the browser will use the image source defined with width description or 300w. When the viewport width is between 300px and 768px, the browser will use the source for 768w, and so on. And lastly, if the viewport width does not satisfy any of the media queries, it will default to a source of 1280w.
We just saw that by adding two new attributes to the <img> tag, we can easily achieve responsive images, where the browser will do the heavy lifting and determine the right image to render. And that will work fine on old browsers that do not support the srcset and sizes attributes, as they will be ignored, and the original image will be used.
Previously we have had 0ur whole site implemented with this, but if you want to try it for yourself, have a look at this pen: https://codepen.io/snowballdigital/pen/KQGvYJ?editors=0010
With the srcset attribute, we define multiple sources with different widths. But what about the case where we have multiple sources for the same width? For example, we have our image in jpeg, WebP, or AVIF format, and we want the browser to pick the best match i.e. select an image based on the viewport width and supported media type.
For browsers supporting AVIF, like Chrome, we want the browser to serve the AVIF format as it offers the best tradeoff between quality and size. And if the browser does not support any modern formats, then we want it to fall back to the jpeg format. This is where the picture and source tags can help us. Let’s see how this can be implemented in React:
function ResponsiveImage() {
return (
<picture>
<source
type="image/avif"
srcSet="image-url-300.avif 300w, image-url-768.avif 768w, image-url-1280.avif 1280w"
sizes="(max-width: 300px) 300px, (max-width: 768px) 768px, 1280px"
/>
<source
type="image/jpeg"
srcSet="image-url-300.jpg 300w, image-url-768.jpg 768w, image-url-1280.jpg 1280w"
sizes="(max-width: 300px) 300px, (max-width: 768px) 768px, 1280px"
/>
<img src="image-url-1280.jpg" />
</picture>
);
}
The picture tag here takes multiple source tags. The source tag takes the same srcset and sizes attributes as the image tag, but it also takes one additional attribute type which hints to the browser what is the media type of the image sources defined for this source. When the browser renders the picture tag, it will look at the tags inside, top to bottom, and the first one that satisfies all the criteria will be used as a source of the image.
In this case, we have the AVIF type defined first because we want the browser to use that one if it is supported. If the browser does not support AVIF, it will look at the next source, and so on. If none of the types is supported, the browser will fallback to the original image as it is defined last in the picture tag. As for the old browsers that do not support picture and source tags, they will be ignored, and the <img> tag with the original source will be used.
This technique for using responsive images becomes very powerful due to its declarative nature. We describe all constraints that should be considered when rendering an image, and the browser will determine and use the one that matches the best. One downside is that it may become quite verbose if we support several formats with multiple breakpoints. Another problem is that these images must be generated in different formats and sizes, as described in the sources.
In Crystallize, we solve these problems for you, so much so that using responsive images becomes a breeze. When you upload an image to Crystallize, we will generate the image in a few formats, like AVIF and WebP, and in several different sizes so they are ready to be used when needed.
The different image variants can be accessed by URL from everywhere, but the easiest way to use the responsive image is to use our React Image component, as shown below:
import { Image } from '@crystallize/reactjs-components';
const imageFromCrystallize = {
url: '...',
variants: [...]
}
<Image
{...imageFromCrystallize}
sizes="(max-width: 300px) 300w, 700px"
/>
The image component will create the srcset based on the generated types and sizes, and the only thing you need to provide is the sizes attribute so the browser can determine the best match.
Quite often, we want to show different images on mobile and desktop. On mobile, the space is limited, so we want to focus on the important things and remove all the background noise that works fine for the desktop.
To do that, we can use the media attribute on the source tag that provides a new constraint for the browser. Let's see how:
function ResponsiveImage () {
return (
<picture>
<source
media="(max-width: 768px)"
srcSet="image-url-768-cropped”.jpeg 768w"
sizes="768px"
/>
<source
srcSet="image-url-1280-big.jpeg 1280w"
sizes="1280px"
/>
<img src="image-url-1280.jpeg" />
</picture>
);
}
In the above case, because the viewport width is less than 768px, the browser satisfies the media constraint on the source tag, so it will use the image provided in the srcset, which is our cropped image. When the viewport is bigger than that, the first source is ignored, and the browser will use the second source with the bigger image.
If you want to see some more real examples and live coding in action, I suggest you check the Livestream video we recorded for responsive images.
Should you have any more questions about this, join our community and let us know.