Recipe: serve WebP with nginx conditionally

Eugene Lazutkin edited this page Feb 26, 2014 · 13 revisions

WebP is a next generation image format spearheaded by Google, which provides advanced compression options. While it is so much better than legacy formats, it is only supported at the moment of writing (2/23/2014) by Chrome, and Opera on desktops and Android (see Can I use WebP image format? for more details). Firefox may support WebP in future versions.

Practical solution is to serve images conditionally depending on WebP support. This recipe discusses how to do it with nginx.

Goal

Let's assume following:

  1. WebP-capable browsers advertise their capability in HTTP Accept header. This is how we know when we can serve WebP.

  2. Images we want to serve as WebP will be placed in the same directory, and have a following naming schema:

    full-filenamefull-filename.webp

    Examples:

    • image.pngimage.png.webp
    • image.jpgimage.jpg.webp

    This way we avoid name clashes from identically named files with different file extensions.

  3. Not all images may have .webp counterparts. If it happens, we should serve original files.

Solution

Contrary to popular beliefs, conditional serving of images do not require if nor rewrite, and can be implemented with more lightweight map and try_files:

user www-data;

http {

  ##
  # Basic Settings
  ##

  sendfile on;
  tcp_nopush on;
  tcp_nodelay on;

  # IMPORTANT!!! Make sure that mime.types below lists WebP like that:
  # image/webp webp;
  include /etc/nginx/mime.types;
  default_type application/octet-stream;

  gzip on;
  gzip_disable "msie6";

  ##
  # Conditional variables
  ##

  map $http_accept $webp_suffix {
    default   "";
    "~*webp"  ".webp";
  }

  ##
  # Minimal server
  ##

  server {
    listen 80 default_server;
    listen [::]:80 default_server ipv6only=on;

    root /usr/share/nginx/html;
    index index.html;

    # Make site accessible from http://localhost/ or whatever you like
    server_name localhost;

    location ~* ^/images/.+\.(png|jpg)$ {
      root /home/www-data;
      add_header Vary Accept;
      try_files $uri$webp_suffix $uri =404;
    }
  }
}

For your convenience, the snippet above was placed in GitHub Gist.

How it works

Only three parts are important: mime.types should list webp, we should define a variable depending on Accept header, and we should use it in try_files.

mime.types

nginx uses a file to list mappings from file extensions to MIME types. Usually it is called mime.types, and included externally. Make sure that it lists webp:

image/webp  webp;

map

map defines a variable that depends on values of other variables (see ngx_http_map_module for details). This module is included in nginx by default, you don't need to recompile anything.

map $http_accept $webp_suffix {
  default   "";
  "~*webp"  ".webp";
}

This http-level snippet defines a variable called $webp_suffix, which depends on $http_accept (our HTTP Accept header). If the header contains "webp" substring (using a case-insensitive regular expression), than our variable will be set to ".webp", otherwise it will be an empty string.

Strictly speaking defining a default as an empty string is superfluous: this is the default behavior anyway. I added this excessive line here for clarity.

Interesting thing about nginx's variables is that they are all lazily calculated, so we can define a lot of them without slowing down our server --- only variables, which we actually use, will be evaluated. In our case, it means that adding our variable does not affect serving other non-image files.

try_files

This is a workhorse of the solution:

try_files $uri$webp_suffix $uri =404;

This location-level directive checks files conditionally breaking on success:

  1. Checks a file + a possible ".webp" suffix, and serves it, if it is found.
  2. Checks a file as it was requested, and serves it, if it is found.
  3. Sends HTTP404 (AKA "not found"), if not found.

Example

Let's go over it in details. We assume that we have file called image.png and its possible counterpart image.png.webp.

  1. User comes with a WebP-capable browser:
    1. $webp_suffix is set to ".webp".
    2. try_files tries image.png.webp. It is served, if found.
    3. Otherwise try_files tries image.png (the original file). It is served, if found.
    4. Otherwise "not found" is returned.
  2. User comes with a browser that knows nothing about WebP:
    1. $webp_suffix is set to "".
    2. try_files tries image.png. It is served, if found.
    3. Otherwise try_files tries again image.png (the original file). While it is clearly redundant, it is unlikely that image is not found. If this is a common situation for an application you develop, remember that nginx caches files, so it will be amortized.
    4. Otherwise "not found" is returned.

try_files is a core module directive. It may check files on disk, redirect to proxies, or internal locations, and return error codes, all in one directive. See try_files for more details.

Notes

This recipe was used as a starting point for a blog post Serve files with nginx conditionally, which contains expanded details. For example, in its appendix it explains why there is no "Recipe: serve JPEG XR with nginx conditionally".