A very thorough overview of how to write a modern, performant, HTML-driven image component that is as optimised to serve the most appropriate image as possible. There are some very neat tricks in here, though I'll caveat it all with: the article openly admits that the native <picture>
element does everything you would want, but then goes into great detail about an alternative, slightly hacky (albeit clever) workaround using the <img>
element. I can understand where the author is coming from as to their reasoning for the second option, but it does seem that the final conclusion should be to use the <picture>
element in almost all circumstances.
Yes, you avoid an extra element in the DOM using the <img>
technique, but the <picture>
element is intended for this behaviour, which means browsers will actively test for it to work (unlike unofficial <img>
hacks) and any extensions to HTML will likely focus on supporting <picture>
first. You can also look up what your code is doing on MDN or any other developer resource using <picture>
, rather than a single blog article. In other words: short-term engineering gains and DX may lead to long-term technical debt, which feels overlooked.
On the current state of image optimisation on the web:
The HTTPArchive found at least 70% of all websites have an image as the most prominent element, yet only 34% of the web uses<img srcset>
to create responsive & performant images (and even fewer use<picture>
).
On the minimum feature set of a modern, responsive image component:
This brings us to the following checklist:
- ☑️️ Serve different dimensions based on the viewport size (e.g. different images for desktop and mobile)
- ☑️️ Serve different qualities based on the viewport size
- ☑️️ Serve different qualities based on Device-Pixel-Ratio (DPR) / zoom level
- ☑️️ Optional: Deliver different file formats (WebP, AVIF, …)
Example of how to use a <picture>
element to achieve the above:
<picture> <source // `media` contains a CSS media query (MQ) that is used to control which // specific source to render (first true `<source>` wins) media="(-webkit-min-device-pixel-ratio: 1.5)" // `srcset` contains the path to an image and an 'intrinsic width descriptor', // corresponding to the original image width on your device srcset="2x-800.jpg 800w, 2x-1200.jpg 1200w, 2x-1598.jpg 1598w" // `sizes` consists of a CSS MQ condition and the width of the slot // You can also use a viewport width (`vw`) sizes=" (min-width: 1066px) 743px, (min-width: 800px) calc(75vw-57px), 100vw"> <img src="1x.jpg" alt=""> </picture>
In this example, for old browsers and below 1.5x DPR screens, the1x.jpg
image is loaded. For other screens, browsers differentiate based on the viewport width, so modern phones load2x-800.jpg
, desktops2x-1598.jpg
.
Example of the <img>
element replicating the native functionality of <picture>
, written in an automatable way; here N
, M
, and A
are used to mean the image widths and the minimum "mobile/desktop" breakpoint (A
):
<img sizes=" (max-width: Apx) and (resolution: 1dppx) Npx, (min-width: (A+1)px) and (resolution: 1dppx) Mpx, (max-width: Apx) and (min-resolution: 2dppx) (M+1)px, (min-width: (A+1)px) and (min-resolution: 2dppx) (((M+1)*5)+1)px" srcset=" low-dpr-xs.jpg Nw, low-dpr-xl.jpg Mw, high-dpr-xs.jpg ((M+1)*5)w, high-dpr-xl.jpg (((M+1)*5)+1)w" src="fallback.jpg" alt="don't forget the alt attribute" />
On how the above works:
By combiningsizes
andsrcset
(width descriptors), we get back the control of what browsers do. As mentioned earlier, the width descriptors work implicitly, so we introduce a specially craftedsizes
attribute that targets individual DPRs to help us make it explicit again. Our crafted<img>
tag now behaves like a<picture>
tag, with no additional tags required. We can conditionally load high quality images in different dimensions and different qualities.
On some research into how low fidelity you can go before people notice/complain:
...we’ve tested how different JPEG image qualities are perceived and found no perceiptable difference betweeen 50% and 35% quality for smartphones with 2x DPR screens. Same for 1x screens in general, where 75% works fine for us.