Servir des images WebP & AVIF avec Nginx

Vincent Bernat

WebP et AVIF sont deux for­mats d’image pour le web. Ils visent à pro­duire des fi­chiers plus pe­tits que les for­mats JPEG et PNG. Ils sup­portent tous deux la com­pres­sion avec ou sans perte, ainsi que la trans­pa­rence al­pha. WebP a été dé­ve­loppé par Google et est un dé­rivé du for­mat vi­déo VP81. Il est sup­porté par la plu­part des na­vi­ga­teurs. AVIF uti­lise le for­mat vi­déo AV1, plus ré­cent, pour ob­te­nir de meilleurs ré­sul­tats. Il est sup­porté par les na­vi­ga­teurs ba­sés sur Chro­mium et a une prise en charge ex­pé­ri­men­tale pour Firefox.

Mise à jour (12.2021)

Fi­re­fox prend en charge AVIF de ma­nière ex­pé­ri­men­tale de­puis la ver­sion 77 (juin 2020) et par dé­faut de­puis la ver­sion 93 (oc­tobre 2021).

Mise à jour (10.2026)

Sa­fari prend en charge AVIF de­puis la ver­sion 16.1 (oc­tobre 2022). JPEG XL est un autre for­mat d’image, conçu pour rem­pla­cer JPEG. Sa­fari le prend en charge de­puis la ver­sion 17 (sep­tembre 2023). Chrome l’a ac­ti­vée par dé­faut dans la ver­sion 155 (oc­tobre 2026). Fi­re­fox l’a ac­ti­vée dans la ver­sion 158 (oc­tobre 2026).

Votre na­vi­ga­teur ne sait dé­co­der au­cun des for­mats WebP, AVIF et JPEG XL. Votre na­vi­ga­teur ne sait dé­co­der que le for­mat WebP. Votre na­vi­ga­teur ne sait dé­co­der que le for­mat AVIF. Votre na­vi­ga­teur ne sait dé­co­der que le for­mat JPEG XL. Votre na­vi­ga­teur sait dé­co­der les for­mats WebP et AVIF, mais pas JPEG XL. Votre na­vi­ga­teur sait dé­co­der les for­mats WebP et JPEG XL, mais pas AVIF. Votre na­vi­ga­teur sait dé­co­der les for­mats AVIF et JPEG XL, mais pas WebP. Votre na­vi­ga­teur sait dé­co­der les for­mats WebP, AVIF et JPEG XL.

Sans Java­Script, je ne peux pas dé­ter­mi­ner les for­mats re­con­nus par votre na­vigateur.

Conversion et optimisation des images#

Pour ce blog, j’uti­lise les com­mandes sui­vantes pour conver­tir et op­ti­mi­ser les images JPEG et PNG. Sau­tez à la sec­tion sui­vante si vous n’êtes in­té­ressé que par la confi­gu­ra­tion de Nginx.

Images JPEG#

Les images JPEG sont conver­ties au for­mat WebP avec cwebp.

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

Elles sont conver­ties au for­mat AVIF avec avifenc de libavif:

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

En­suite, elles sont op­ti­mi­sées avec jpe­gop­tim com­pilé avec l’en­co­deur amé­lioré de Mo­zilla, via Nix. C’est une des rai­sons pour les­quelles j’aime 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

Images PNG#

Les images PNG sont sous-échan­tillon­nées vers une pa­lette RGBA 8 bits avec png­quant. Cette conver­sion ré­duit consi­dé­ra­ble­ment la taille des fi­chiers tout en étant pra­ti­que­ment 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

En­suite, elles sont conver­ties au for­mat WebP avec cwebp dans le mode sans perte.

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

Au­cune conver­sion n’est ef­fec­tuée vers AVIF : la com­pres­sion sans perte n’est pas aussi ef­fi­cace que pngquant et la com­pres­sion avec perte n’est que mar­gi­na­le­ment meilleure par rap­port à ce que j’ob­tiens avec WebP.

Garder uniquement les fichiers les plus petits#

Je ne garde les images WebP et AVIF que si elles sont au moins 10 % plus pe­tites que le for­mat ori­gi­nal : le dé­co­dage est gé­né­ra­le­ment plus ra­pide pour les JPEG et PNG et les images JPEG peuvent être dé­co­dées pro­gres­si­vement2.

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

Je ne garde les images au for­mat AVIF que lorsqu’elles sont plus pe­tites que le for­mat 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

Nous pou­vons com­pa­rer com­bien d’images sont conser­vées après conver­sion en WebP ou 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 se com­porte mieux que MozJPEG pour la plu­part des fi­chiers JPEG alors que WebP ne fait mieux que pour un fi­chier sur deux :

       Original       WebP       AVIF
 PNG         64         47          0
 JPG         83         40         74

Informations complémentaires#

Je n’ai pas dé­taillé mes choix pour les pa­ra­mètres de qua­lité et il n’y a pas beau­coup de science là-de­dans. Voici deux res­sources qui donnent plus de dé­tails sur AVIF :

Servir WebP & AVIF avec Nginx#

Pour ser­vir les images WebP et AVIF, il existe deux pos­si­bilités :

  • uti­li­ser pour lais­ser le na­vi­ga­teur choi­sir le for­mat qu’il prend en charge ;
  • uti­li­ser la né­go­cia­tion de contenu pour lais­ser le ser­veur en­voyer le for­mat le mieux supporté.

J’uti­lise la deuxième ap­proche. Elle re­pose sur l’ins­pec­tion de l’en-tête HTTP Accept dans la re­quête. Pour Chrome, elle res­semble à ceci :

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

Je confi­gure Nginx pour qu’il serve l’image AVIF, puis l’image WebP et se ra­batte sur l’image JPEG/PNG ori­gi­nale en fonc­tion de ce que le na­vi­ga­teur annonce3 :

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;
  }
}

Par exemple, sup­po­sons que le na­vi­ga­teur de­mande /images/​ont-box-orange@2x.​jpg. S’il prend en charge WebP mais pas AVIF, $webp_​suffix est dé­fini à .webp tan­dis que $avif_​suffix est la chaîne vide. Le ser­veur es­saie de ser­vir le pre­mier fi­chier exis­tant dans cette liste :

  • /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.

Si le na­vi­ga­teur prend en charge les deux for­mats d’image, Nginx par­court la liste suivante :

  • /images/​ont-box-orange@2x.​jpg.​webp.​avif (il n’existe jamais) ;
  • /images/​ont-box-orange@2x.​jpg.avif ;
  • /images/​ont-box-orange@2x.​jpg.webp ;
  • /images/​ont-box-orange@2x.jpg.

Eu­gene La­zut­kin ex­plique plus en dé­tails com­ment cela fonc­tionne. Je n’ai pré­senté ici qu’une va­ria­tion pre­nant en charge à la fois WebP et AVIF.