Serving & negotiating formats

Two ways to deliver next-gen image formats safely

Two approaches to safe format delivery

Browser support for the newest image formats — AVIF, WebP, JPEG XL — is excellent in modern browsers but is never 100% across the full range of devices in the wild. You cannot just swap your .jpg references for .avif and call it done: a browser that does not understand AVIF will render a broken image. You need a strategy for serving the best available format to each visitor while guaranteeing a working fallback.

There are two fundamentally different strategies, and choosing between them is a practical architecture decision:

  • Client-side selection with <picture> — you ship multiple versions of each image (AVIF, WebP, JPEG) and let the browser choose. The server just serves whatever file is requested. No server configuration needed.
  • Content negotiation — the browser sends the request, the server inspects which formats the browser accepts, and returns the best format at a single URL. The server (or CDN) does the work.

Side-by-side comparison

Property <picture type> (client-side) Content negotiation (server-side)
Where the decision happens In the browser, before any network request On the server / CDN, after the request arrives
Number of image URLs One per format variant (multiple URLs in the HTML) One URL; format is transparent to the page
Server configuration required None — any static file host works Yes — server must read Accept and vary the response
HTML complexity Grows with the number of format variants Stays simple — just a plain <img src>
Cache behaviour Each URL caches independently Requires Vary: Accept so caches store one copy per format
Best fit for Static sites, no back-end, small image sets Dynamic sites, large image libraries, CDN pipelines

Tutorial 07: The picture element & art direction covered the <picture type> approach in depth — including why combining format, crop, and colour-scheme variants can require a large number of <source> elements. This tutorial picks up from there and looks at the server-side alternative.

Content negotiation

Content negotiation is a standard HTTP mechanism in which the client advertises what it can handle in a request header and the server responds with the best matching representation. It is not specific to images — the same mechanism drives language selection (Accept-Language) and compression (Accept-Encoding for gzip/Brotli). Images use the Accept header.

What the browser sends

Every image request from a modern browser includes an Accept header listing the MIME types it is willing to receive, ordered roughly by preference:

GET /images/hero.jpg HTTP/1.1 Host: example.com Accept: image/avif,image/webp,image/apng,image/*,*/*;q=0.8

A browser that supports AVIF lists image/avif first. One that supports WebP but not AVIF leads with image/webp. An older browser may only send image/*, meaning "any image format." The server reads this header to decide what to return.

What the server returns

If the server is configured to handle format negotiation, the response for that same URL looks like this for an AVIF-capable browser:

HTTP/1.1 200 OK Content-Type: image/avif Content-Length: 24312 Vary: Accept Cache-Control: public, max-age=31536000, immutable

And for a browser that only supports WebP:

HTTP/1.1 200 OK Content-Type: image/webp Content-Length: 38740 Vary: Accept Cache-Control: public, max-age=31536000, immutable

The URL is identical in both cases. The difference in the response is transparent to the HTML — the <img src> just references /images/hero.jpg and each browser silently receives the best format it can handle.

Why Vary: Accept is essential

This is the part most often overlooked. HTTP caches — both the browser's own cache and any intermediate CDN or proxy — key their stored responses on the request URL by default. If the server returns AVIF for one request and WebP for the next, and there is no Vary header, a cache might store the first response (AVIF) and serve it to a browser that asked for WebP — which would render a broken image.

The Vary: Accept response header tells caches: "store a separate cached copy for each distinct Accept value." The cache key becomes URL + Accept header combination, so the right format is always served to the right client. Without it, content negotiation is broken at scale.

The same idea applies to compression and language

Accept-Encoding: gzip, br is content negotiation for compression: the browser advertises support for gzip and Brotli, and the server compresses the response body accordingly. Accept-Language: en-GB, en;q=0.9 is the same idea for internationalised content. The image negotiation described above is the same pattern applied to image formats. Once you understand one flavour of content negotiation you understand all of them.

Image CDNs

Setting up per-format serving on your own server is non-trivial: you need to pre-generate each format variant, configure the web server or application to read Accept, and add Vary: Accept correctly. For sites with hundreds or thousands of images, a more practical solution is an image CDN.

Services like Cloudinary and Imgix are purpose-built for this. Instead of uploading multiple copies of an image, you upload one original and request variants on the fly via URL parameters. The CDN handles conversion and resizing at its edge servers — machines geographically close to each user — so the optimised image is served from a nearby node rather than a distant origin server. This matters for latency; it is the same "from nearby" principle described in 01: Why images matter.

URL-parameter APIs

Each image CDN has its own parameter syntax, but the concepts are consistent. A Cloudinary URL might look like this:

https://res.cloudinary.com/your-cloud/image/upload/f_auto,q_auto,w_800/hero.jpg

The transformation parameters here are:

  • f_auto — automatically pick the best format for the requesting browser (AVIF, WebP, or JPEG as appropriate)
  • q_auto — automatically pick a compression quality level
  • w_800 — resize to 800 px wide

An Imgix URL follows similar logic:

https://your-site.imgix.net/hero.jpg?auto=format,compress&w=800

The CDN performs content negotiation on your behalf — it reads the browser's Accept header and returns image/avif, image/webp, or image/jpeg accordingly. You write a single URL; the CDN handles the rest, sets Vary: Accept, and stores each variant in its edge cache.

Using CDN URLs in responsive markup

CDN URLs compose cleanly with srcset because each size is just a different URL parameter. You get responsive images and automatic format selection without managing any files yourself:

<img src="https://res.cloudinary.com/your-cloud/image/upload/f_auto,q_auto,w_800/hero.jpg" srcset=" https://res.cloudinary.com/your-cloud/image/upload/f_auto,q_auto,w_400/hero.jpg 400w, https://res.cloudinary.com/your-cloud/image/upload/f_auto,q_auto,w_800/hero.jpg 800w, https://res.cloudinary.com/your-cloud/image/upload/f_auto,q_auto,w_1200/hero.jpg 1200w " sizes="(max-width: 600px) 100vw, 800px" alt="Descriptive alt text" width="800" height="533" loading="lazy" decoding="async" >

Each entry in srcset is the same image at a different width. The CDN applies f_auto to every request, so an AVIF-capable browser on a fast connection downloads a small AVIF, while an older device gets a JPEG — all from the same short list of URLs.

The dependency trade-off

Image CDNs are a third-party dependency. If the CDN has an outage, your images go down with it. There are also cost considerations: most services charge per image transformation or per bandwidth consumed, so costs can grow with traffic. For a small personal project or a simple marketing site, the <picture> approach with locally-stored files is often the right call. For a publishing platform, an e-commerce catalogue, or any site where images are core to the product, an image CDN typically pays for itself in reduced file sizes, engineering time saved, and the improved performance that comes from edge delivery.

TL;DR — which approach should you use?

There is no single right answer, but the decision is usually straightforward:

  • Static site or small image set: use <picture> with <source type>. Pre-generate AVIF and WebP alongside your JPEG originals (ImageMagick, Squoosh, or your build tool can automate this), write the <picture> markup once, and you are done. No server configuration, no third-party accounts.
  • Many images or a dynamic image pipeline: content negotiation at the server or CDN layer removes the need to write per-format HTML. The HTML stays clean (<img src>), and the server handles the rest. Requires proper server setup and correct Vary: Accept headers — see the server-side section and the HTTP section for the mechanics.
  • Image-heavy application or need for edge delivery: an image CDN (Cloudinary, Imgix, or a similar service) is the pragmatic choice. You get automatic format selection, on-the-fly resizing, and edge caching for one subscription cost. The URL-parameter API replaces both the server configuration and the need for multiple files.

The unifying principle: the browser will always receive the format it asked for — the question is only who makes that decision and where. On the client, it is the <picture> element. On the server, it is content negotiation. At the edge, it is an image CDN. All three achieve the same goal.