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 srcset and 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.

Hero image position without preloading
Chrome Firefox Safari Edge
Position in waterfall 6 6 4 5

Preloading with href

Preloading responsive images is analogous to loading responsive images. The preload link can have imagesrcset and imagesizes attributes that take the same values as srcset and 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">

(I’m leaving alt , width and 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 imagesrcset or 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.

Hero image position with preload and href attribute
Chrome Firefox Safari Edge
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">

Hero image position with preload and empty href
Chrome Firefox Safari Edge
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.

Hero image position with preload and missing href
Chrome Firefox Safari Edge
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.

Scripting

I had thought of writing some JS to fix Safari, but decided against it. Using srcset and 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.

*In smaller viewports, which is usually on mobile devices, the cookie consent banner can be the LCP element, but the tests were all scripted to use a cookie that keeps it closed.