JXL, AVIF, WebP
Serve the Optimal Image Format with Nginx

by Eric Fortis

Why let Nginx determine the best image format to serve?

  1. There’s no need to change client-side code.
  2. Unlike <img> tags, <video> posters have no standard 1 way of specifying many URLs for fetching the optimal format supported by the client.

Here’s how

Upload the images with the modern extension appended.

  • foo.png
  • foo.png.jxl
  • foo.png.avif
  • foo.png.webp

By the way, here’s a script to convert them.

For dispatching the most efficient format, Nginx will inspect the Accept request header. For example, Chrome 85 and up advertise they support AVIF by sending its media (MIME) type in it:

Accept: image/avif,image/webp,image/apng,image/*,*/*;q=0.8

AVIF supported: Chrome 85, Firefox 93, and Safari 16

nginx.conf 2

http {
  # …
  map $http_accept $img_ext {
    ~image/jxl   '.jxl';
    ~image/avif  '.avif';
    ~image/webp  '.webp';
    default      '';
  }

  server {
    # …
    location /images {
      add_header Vary Accept;
      try_files  $uri$img_ext $uri =404;
    }
  }
}

On requests within the images/ directory, the custom variable $img_ext gets defined with the extension associated to the first matched MIME regex of the map. For that, the map uses the Accept header ($http_accept) as input parameter.

Then, try_files looks for a file with that extension appended, such as foo.png.avif, and serves it if such file exists. On the other hand, the middle $uri is a fail-safe. For instance, maybe the AVIF isn’t uploaded yet. Also, it makes possible to request a format directly, e.g. GET images/foo.png.webp

Lastly, Vary: Accept is set as a response header. This is only needed if the server has caching reverse proxies in front – e.g., intercepting CDNs. Otherwise, those proxies would always serve the same format sent to the first client that requested the particular image.3

conf/mime.types

Make sure the media types are registered.

types {
  # …
  image/jxl   jxl;
  image/avif  avif;
}

Open Source

The actual nginx.conf Uxtly uses.

How to test it?

lenna test image
#!/bin/sh

img=lenna-colors.png

for mime in image/avif image/png; do
  response_mime=$(curl -so /dev/null --head \
    --header "Accept: $mime" \
    --write-out "%{content_type}" $img)

  if [ "$response_mime" = $mime ]; then
    echo "OK $mime"
  else
    echo "FAIL $mime Got: $response_mime" >&2
    exit 1
  fi
done

References

1. C. Morgan (2023) Video Poster using SVG’s foreignObject 2. E. Lazutkin (2014) Serve files with Nginx conditionally 3. R. Mulhuijzen (2014) Best Practices for Using the Vary Header

Sponsored by: