Preloading responsive images
tl;dr use an empty href attribute in responsive image preloads for the best results across all browsers, like so:
<link rel="preload" href="" imagesrcset="small.jpg 500w, medium.jpg 1000w, large.jpg 1500w" imagesizes="100vw">.
I’m working on a pretty big performance project for our biggest site at work and have been using a Cloudflare worker to test out an idea for reducing our LCP.
In every viewport size* the LCP element is the main hero image. It’s a responsive image that uses
sizes to serve an appropriately sized image to each user.
Until a few days ago there was no image preload done for the hero, and LCP was 4.296 seconds over 3G on mobile. The hero image was loading between positions 4-6 in browsers’ network waterfalls.
|Position in waterfall||6||6||4||5|
Preloading with href
Preloading responsive images is analogous to loading responsive images. The preload link can have
imagesizes attributes that take the same values as
sizes on the image like so:
<img src="small.jpg" srcset="small.jpg 500w, medium.jpg 1000w, large.jpg 1500w" sizes="100vw">
<link rel="preload" href="small.jpg" imagesrcset="small.jpg 500w, medium.jpg 1000w, large.jpg 1500w" imagesizes="100vw">
height off the
<img> to make this particular example clearer, but they should always be included.)
That was my first attempt at the
<link>, but Safari doesn’t support
imagesizes, so preloads small.jpg no matter what size the viewport is. In wider viewports this means a double download: the preloaded small image and the appropriately sized image.
It improved the position of the image in the waterfall in every browser except Safari, where the double download pushed the correct image down one position.
|Position in waterfall||2||4||5||2|
Preloading with empty href
So I changed the markup to use an empty
href attribute. This meant Safari got the image in its original non-preloaded position in the waterfall, and the other browsers retained the benefit of the preload.
<link rel="preload" href="" imagesrcset="small.jpg 500w, medium.jpg 1000w, large.jpg 1500w" imagesizes="100vw">
|Position in waterfall||2||4||4||2|
This got us a 2.877 second LCP, which is a nice improvement.
Preloading with missing href
At this point I went to the spec to check if an empty href is allowed, and lo and behold it isn’t. It states that “its value must be a valid non-empty URL”. However it also says that it can be left out entirely if
imagesrcset is present.
So I changed the link to:
<link rel="preload" imagesrcset="small.jpg 500w, medium.jpg 1000w, large.jpg 1500w" imagesizes="100vw">
Unfortunately Firefox doesn’t have that implemented, and the image went back down to number 6 in its waterfall. The two Blink-powered browsers I tested (Chrome and Edge) preloaded the image ok, and obviously Safari was unchanged.
|Position in waterfall||2||6||4||2|
So for now I’ll be using the empty
href="", even though that’s not how it’s specced, and have filed a bug in Firefox’s bug tracker.
I had thought of writing some JS to fix Safari, but decided against it. Using
sizes on an
<img> is not the same as using media queries. It’s giving the browser a list of image sources and sizes, and letting it decide the best one to pick based on viewport width, device pixel ratio, and—theoretically at least—network conditions.
If we were using the
<picture> element with media queries it would be easy to write a script to polyfill Safari, but without the clear and deliberate instructions
<picture> gives it’s not possible to know which image will be loaded.
I think it would be easy enough to test for
imagesrcset support using some DOM scripting, but the method I’m using isn’t doing any harm in Safari, so I’m happy enough to continue with the empty
href until Firefox and Safari fully support the specification.