There are three formats worth serving a photograph in today, every browser supports a different subset, and the browser tells you which on every single request. Content negotiation is the mechanism, it is nearly thirty years old, and it is still the tidiest way to solve this — provided you get the caching right, which is where it usually goes wrong.
What the browser actually tells you
Every image request carries an Accept header. A current Chrome sends something
close to:
Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
That is a statement of capability in preference order. Safari's list differs, an older
browser's is shorter, and a crawler may send */* and mean it. The header is the
only trustworthy signal here — considerably more so than the User-Agent string, which is a
thicket of compatibility lies by design.
So the rule is simple: pick the best format the client has actually claimed to support. AVIF if offered, else WebP, else fall back to JPEG or PNG depending on whether the source has transparency.
The part that breaks: Vary
Here is the failure mode, and it is common enough to be worth stating in full.
A cache — your CDN, a corporate proxy, the browser's own — stores a response against a URL. If two clients request the same URL and you served them different bytes based on a request header, the cache has no way to know that unless you tell it. So the first visitor with AVIF support poisons the entry, and the next visitor on an older browser is handed an AVIF file it cannot decode. A broken image icon, on a page that works perfectly for whoever tested it.
Vary: Accept
That header is the whole fix. It tells every cache in the chain that this response depends on
the request's Accept header, so entries must be keyed by it. Serving negotiated
images without Vary is not a subtle performance issue — it is a correctness bug
that only appears for the minority of users on a different browser than yours.
If you take one thing from this article: negotiate if you like, but never negotiate withoutVary. Serving one format to everybody is worse but honest; negotiating withoutVaryis silently wrong for some fraction of your traffic.
Negotiation versus <picture>
The alternative is doing it in markup, with the browser choosing between explicit sources:
<picture>
<source srcset="hero.avif" type="image/avif">
<source srcset="hero.webp" type="image/webp">
<img src="hero.jpg" alt="…">
</picture>
Both approaches are legitimate, and they trade different things:
| Negotiation | <picture> | |
|---|---|---|
| Markup | One <img>, unchanged | Three sources per image |
| Cache entries | One URL, several variants | One URL per format |
| Debuggability | Depends on a header you cannot see in the address bar | Obvious from the page source |
| Fails when | Vary is missing or a proxy ignores it | Never, really — it is explicit |
<picture> is the more predictable of the two and is the right choice when
you control the markup and the image set is small. Negotiation wins when the markup is
generated by something that does not know what formats exist — a CMS, a comment system, a
template written years ago.
A note on AVIF's cost
AVIF encodes smaller and it encodes slowly — meaningfully slower than WebP, which is itself slower than JPEG. On a cache miss that time is on the request path.
This does not argue against AVIF. It argues for caching it properly, and for being deliberate about how many distinct AVIF renditions your URLs can produce, because each one is paid for separately the first time it is asked for. Decode, in contrast, is fast enough on the client to be a non-issue on anything made in the last several years.
What we do here
f=auto reads the Accept header and picks the best format the client
offered, and the response carries Vary: Accept. The cache key includes the
negotiated format, so an AVIF rendition and a WebP rendition of the same URL are two entries
rather than one entry that is wrong for half its audience.
If you would rather be explicit, name the format — f=webp — and you get exactly
that, for everybody, cached as one thing.