Serving WebP & AVIF images with Nginx

Vincent Bernat

WebP and AVIF are two im­age for­mats for the web. They aim to pro­duce smaller files than JPEG and PNG. They both sup­port lossy and loss­less com­pres­sion, as well as al­pha trans­parency. WebP was de­vel­oped by Google and is a de­riv­a­tive of the VP8 video for­mat.1 It is sup­ported on most browsers. AVIF is us­ing the newer AV1 video for­mat to achieve bet­ter re­sults. It is sup­ported by Chromium-based browsers and has ex­per­i­men­tal sup­port for Firefox.

Up­date (2021-12)

AVIF sup­port for Fire­fox was be­hind a flag since Fire­fox 77 (June 2020) and is en­abled by de­fault since Fire­fox 93 (Oc­to­ber 2021).

Up­date (2026-10)

Sa­fari sup­ports AVIF since Sa­fari 16.1 (Oc­to­ber 2022). JPEG XL is an­other im­age for­mat, de­signed to re­place JPEG. Sa­fari sup­ports it since Sa­fari 17 (Sep­tem­ber 2023). Chrome en­abled it in Chrome 155 (Oc­to­ber 2026). Fire­fox en­abled it in Fire­fox 158 (Oc­to­ber 2026).

Your browser sup­ports none of the WebP, AVIF, and JPEG XL im­age for­mats. Your browser only sup­ports the WebP im­age for­mat. Your browser only sup­ports the AVIF im­age for­mat. Your browser only sup­ports the JPEG XL im­age for­mat. Your browser sup­ports the WebP and AVIF im­age for­mats, but not JPEG XL. Your browser sup­ports the WebP and JPEG XL im­age for­mats, but not AVIF. Your browser sup­ports the AVIF and JPEG XL im­age for­mats, but not WebP. Your browser sup­ports the WebP, AVIF, and JPEG XL im­age formats.

With­out Java­Script, I can’t tell what your browser supports.

Converting and optimizing images#

For this blog, I am us­ing the fol­low­ing shell snip­pets to con­vert and op­ti­mize JPEG and PNG im­ages. Skip to the next sec­tion if you are only in­ter­ested in the Ng­inx setup.

JPEG images#

JPEG im­ages are con­verted to WebP us­ing cwebp.

find media/images -type f -name '*.jpg' -print0 \
  | xargs -0n1 -P$(nproc) -i \
      cwebp -q 84 -af '{}' -o '{}'.webp

They are con­verted to AVIF us­ing avifenc from libavif:

find media/images -type f -name '*.jpg' -print0 \
  | xargs -0n1 -P$(nproc) -i \
      avifenc --codec aom --yuv 420 --min 20 --max 25 '{}' '{}'.avif

Then, they are op­ti­mized us­ing jpe­gop­tim built with Mozilla’s im­proved JPEG en­coder, via Nix. This is one rea­son I love Nix.

jpegoptim=$(nix-build --no-out-link \
      -E 'with (import <nixpkgs>{}); jpegoptim.override { libjpeg = mozjpeg; }')
find media/images -type f -name '*.jpg' -print0 \
  | sort -z
  | xargs -0n10 -P$(nproc) \
      ${jpegoptim}/bin/jpegoptim --max=84 --all-progressive --strip-all

PNG images#

PNG im­ages are down-sam­pled to 8-bit RGBA-pal­ette us­ing pngquant. The con­ver­sion re­duces file sizes sig­nif­i­cantly while be­ing mostly in­visible.

find media/images -type f -name '*.png' -print0 \
  | sort -z
  | xargs -0n10 -P$(nproc) \
      pngquant --skip-if-larger --strip \
               --quiet --ext .png --force

Then, they are con­verted to WebP with cwebp in loss­less mode:

find media/images -type f -name '*.png' -print0 \
  | xargs -0n1 -P$(nproc) -i \
      cwebp -z 8 '{}' -o '{}'.webp

No con­ver­sion is done to AVIF: loss­less com­pres­sion is not as ef­fi­cient as pngquant and lossy com­pres­sion is only marginally bet­ter than what I get with WebP.

Keeping only the smallest files#

I am only keep­ing WebP and AVIF im­ages if they are at least 10% smaller than the orig­i­nal for­mat: de­cod­ing is usu­ally faster for JPEG and PNG; and JPEG im­ages can be de­coded pro­gres­sively.2

for f in media/images/**/*.{webp,avif}; do
  orig=$(stat --format %s ${f%.*})
  new=$(stat --format %s $f)
  (( orig*0.90 > new )) || rm $f
done

I only keep AVIF im­ages if they are smaller than WebP.

for f in media/images/**/*.avif; do
  [[ -f ${f%.*}.webp ]] || continue
  orig=$(stat --format %s ${f%.*}.webp)
  new=$(stat --format %s $f)
  (( $orig > $new )) || rm $f
done

We can com­pare how many im­ages are kept when con­verted to WebP or AVIF:

printf "     %10s %10s %10s\n" Original WebP AVIF
for format in png jpg; do
  printf " ${format:u} %10s %10s %10s\n" \
    $(find media/images -name "*.$format" | wc -l) \
    $(find media/images -name "*.$format.webp" | wc -l) \
    $(find media/images -name "*.$format.avif" | wc -l)
done

AVIF is bet­ter than MozJPEG for most JPEG files while WebP beats MozJPEG only for one file out of two:

       Original       WebP       AVIF
 PNG         64         47          0
 JPG         83         40         74

Further reading#

I didn’t de­tail my choices for qual­ity pa­ram­e­ters and there is not much sci­ence in it. Here are two re­sources pro­vid­ing more in­sight on AVIF:

Serving WebP & AVIF with Nginx#

To serve WebP and AVIF im­ages, there are two pos­si­bilities:

  1. use <picture> to let the browser pick the for­mat it sup­ports, or
  2. use con­tent ne­go­ti­a­tion to let the server send the best-sup­ported format.

I use the sec­ond ap­proach. It re­lies on in­spect­ing the Accept HTTP header in the re­quest. For Chrome, it looks like this:

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

I con­fig­ure Ng­inx to serve the AVIF im­age, then the WebP im­age, and fall­back to the orig­i­nal JPEG/PNG im­age de­pend­ing on what the browser ad­vertises:3

http {
  map $http_accept $webp_suffix {
    default        "";
    "~image/webp"  ".webp";
  }
  map $http_accept $avif_suffix {
    default        "";
    "~image/avif"  ".avif";
  }
}
server {
  # […]
  location ~ ^/images/.*\.(png|jpe?g)$ {
    add_header Vary Accept;
    try_files $uri$avif_suffix$webp_suffix $uri$avif_suffix $uri$webp_suffix $uri =404;
  }
}

For ex­am­ple, sup­pose the browser re­quests /images/​ont-box-orange@2x.​jpg. If it sup­ports WebP but not AVIF, $webp_​suffix is set to .webp while $avif_​suffix is set to the empty string. The server tries to serve the first ex­ist­ing file in this list:

  • /images/​ont-box-orange@2x.​jpg.webp
  • /images/​ont-box-orange@2x.jpg
  • /images/​ont-box-orange@2x.​jpg.webp
  • /images/​ont-box-orange@2x.jpg

If the browser sup­ports both AVIF and WebP, Ng­inx walks the fol­low­ing list:

  • /images/​ont-box-orange@2x.​jpg.​webp.​avif (it never exists)
  • /images/​ont-box-orange@2x.​jpg.avif
  • /images/​ont-box-orange@2x.​jpg.webp
  • /images/​ont-box-orange@2x.jpg

Eu­gene Lazutkin ex­plains in more de­tail how this works. I have only pre­sented a vari­a­tion of his setup sup­port­ing both WebP and AVIF.