Deploying WebP via Accept Content Negotiation

Deploying new image formats on the web is a challenge, but not an unsolvable one, and one that would pay high dividends in terms of performance. In fact, there are many ways to tackle the problem: client-side logic, server negotiation, and hybrid strategies. There is a time and place for each one - they are not exclusive.

Having said that, I'm partial to server negotiation as it requires the least amount of work to move the web forward in a significant way: a single flip of a switch by a CDN or a large hosting provider, instantly enabling significant byte savings for all of their users. Computers are great at performing routine negotiation work, humans are not: we get bored, we get lazy, we routinely forget to optimize our images.

WebP deployment checklist

As a hands on example, let's take a look at what we would need to enable Accept negotiation for deploying WebP:

  1. User agents which support WebP should advertise it in the Accept header
  2. Servers and caches should use the Accept header to serve appropriate assets
  3. Origin servers must specify Vary: Accept in generated responses

Most importantly, notice what's missing: there is no mention of having to modify the markup or logic of millions of individual web applications. Having said that, the above steps still require some work, let's dig a bit deeper.

Configuring Accept negotiation in Nginx

The great news is that Chrome Canary is now advertising image/webp in its Accept header for all image requests - with that in place, we can check off the first item on our checklist above. Now, we need to configure our Nginx server to automatically serve the right file. To start, let's assume we have a pre-generated a WebP asset on disk:

location / {
  # check Accept header for webp, check if .webp is on disk
  if ($http_accept ~* "webp")    { set $webp_accept "true"; }
  if (-f $request_filename.webp) { set $webp_local  "true"; }

  # if WebP variant is available, serve Vary
  if ($webp_local = "true") {
    add_header Vary Accept;

  # if WebP is supported by client, serve it
  if ($webp_accept = "true") {
    rewrite (.*) $1.webp break;
  # ...

First, we check if the Accept header is advertising WebP. Then we check if there is a corresponding file with a .webp extension on disk. If both conditions match, we serve the WebP asset and add Vary: Accept - done.

Now, let's consider the case where Nginx acts as a proxy cache. Here we need to teach Nginx to cache and serve multiple variants of the same image (WebP and non-WebP versions) based on the value of the Accept header:

server {
  location / {
    # lookup inbound requests with exta WebP flag if advertised in Accept
    if ($http_accept ~* "webp") { set $webp T; }
    proxy_cache_key $scheme$proxy_host$request_uri$webp;

    # proxy requests to remote servers
    proxy_pass http://backend;
    proxy_cache my-cache;

Not much different from the previous example! Instead of checking the local disk for the asset, we pass the request through to some set of backends, and append the WebP bit to our cache key - that's all there is to it.

Accept fragmentation and Key

Wait, why didn't we just append the $http_accept variable to our proxy cache key above? Unfortunately, the Accept header varies from browser to browser, which means that using the raw header would unnecessarily fragment the cache. Hence, we scan for WebP support in the header and add an additional bit to the cache key.

However, wouldn't it be nice if we had a standard mechanism to define a custom cache key? Well, good news, there is work underway on the new Key response header, which will act as a smart replacement to Vary:

The 'Key' header field for HTTP responses allows an origin server to describe the cache key for a negotiated response: a short algorithm that can be used upon later requests to determine if the same response is reusable.

IE is special, as always!

Unfortunately, Vary support in IE, even in the latest versions, remains very poor: IE does not cache outbound headers, which means it cannot perform the appropriate matching algorithm, and is forced to issue a revalidation request on any asset with Vary. As a result, to avoid the extra requests, we can create a special case for IE:

  if ($http_user_agent ~* "(?i)(MSIE)") {
    # drop Vary for IE users, mark resource as private
    proxy_hide_header Vary;
    add_header Cache-Control private;

With the above configuration in place, IE users won't see Vary, and resource is marked as private: it can still be cached on the client, but it won't be cached by any other intermediate proxy.

The "action plan", continued...

Small steps, but we're heading in the right direction. We've addressed the issue on the client, we know how to configure our servers, now we need to push for better support by proxies and CDN's - ideally, by deploying support for Key! The good news is, this requires work, but it is not rocket science. As a case in point, CDNConnect recently enabled transparent Accept+WebP negotiation for all of their users.

Finally, let's take a look at Facebook's recent experiment with WebP, as summarized by ArsTechnica:

It turns out that Facebook users routinely do things like copy image URLs and save images locally... As a result, users were left with unusable URLs and files; files they couldn't open locally, and URLs that didn't work in Internet Explorer, Firefox, or Safari.

URL sharing is not an issue if Accept negotiation is configured correctly - see instructions above. Next, once a file is saved locally, you need a local WebP codec or viewer. The good news is, Chrome just landed a patch to register itself as a default WebP handler across all platforms - if you have Chrome, you can view local WebP files, and there is a growing list of editors, viewers, and plugins. Also, there are ongoing discussions about enabling the browser to save a "safe copy" of the asset to mitigate the problem entirely. One step at a time, we're getting there!

Update: configuring Apache to serve WebP with Accept negotiation, courtesy of Sergej Müller.

Ilya GrigorikIlya Grigorik is a web ecosystem engineer, author of High Performance Browser Networking (O'Reilly), and Principal Engineer at Shopify — follow on Twitter.