Servir des images WebP & AVIF avec Nginx

Vincent Bernat

WebP et AVIF sont deux formats d’image pour le web. Ils visent à produire des fichiers plus petits que les formats JPEG et PNG. Ils supportent tous deux la compression avec ou sans perte, ainsi que la transparence alpha. WebP a été développé par Google et est un dérivé du format vidéo VP81. Il est supporté par la plupart des navigateurs. AVIF utilise le format vidéo AV1, plus récent, pour obtenir de meilleurs résultats. Il est supporté par les navigateurs basés sur Chromium et a une prise en charge expérimentale pour Firefox2.

Votre navigateur sait décoder les formats WebP et AVIF. Votre navigateur ne sait décoder aucun de ces formats. Votre navigateur ne sait décoder que le format WebP. Votre navigateur ne sait décoder que le format AVIF.

Sans JavaScript, je ne peux pas déterminer les formats reconnus par votre navigateur.

Conversion et optimisation des images#

Pour ce blog, j’utilise les commandes suivantes pour convertir et optimiser les images JPEG et PNG. Sautez à la section suivante si vous n’êtes intéressé que par la configuration de Nginx.

Images JPEG#

Les images JPEG sont converties au format WebP avec cwebp.

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

Elles sont converties au format 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

Ensuite, elles sont optimisées avec jpegoptim compilé avec l’encodeur amélioré de Mozilla, via Nix. C’est une des raisons pour lesquelles 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-échantillonnées vers une palette RGBA 8 bits avec pngquant. Cette conversion réduit considérablement la taille des fichiers tout en étant pratiquement invisible.

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

Ensuite, elles sont converties au format 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

Aucune conversion n’est effectuée vers AVIF : la compression sans perte n’est pas aussi efficace que pngquant et la compression avec perte n’est que marginalement meilleure par rapport à ce que j’obtiens 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 petites que le format original : le décodage est généralement plus rapide pour les JPEG et PNG et les images JPEG peuvent être décodées progressivement3.

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 format AVIF que lorsqu’elles sont plus petites que le format 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 pouvons comparer combien d’images sont conservées après conversion 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 comporte mieux que MozJPEG pour la plupart des fichiers JPEG alors que WebP ne fait mieux que pour un fichier 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 paramètres de qualité et il n’y a pas beaucoup de science là-dedans. Voici deux ressources qui donnent plus de détails sur AVIF :

Servir WebP & AVIF avec Nginx#

Pour servir les images WebP et AVIF, il existe deux possibilités :

  • utiliser pour laisser le navigateur choisir le format qu’il prend en charge ;
  • utiliser la négociation de contenu pour laisser le serveur envoyer le format le mieux supporté.

J’utilise la deuxième approche. Elle repose sur l’inspection de l’en-tête HTTP Accept dans la requête. Pour Chrome, elle ressemble à ceci :

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

Je configure Nginx pour qu’il serve l’image AVIF, puis l’image WebP et se rabatte sur l’image JPEG/PNG originale en fonction de ce que le navigateur annonce4 :

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, supposons que le navigateur demande /images/ont-box-orange@2x.jpg. S’il prend en charge WebP mais pas AVIF, $webp_suffix est défini à .webp tandis que $avif_suffix est la chaîne vide. Le serveur essaie de servir le premier fichier existant 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 navigateur prend en charge les deux formats d’image, Nginx parcourt 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

Eugene Lazutkin explique plus en détails comment cela fonctionne. Je n’ai présenté ici qu’une variation prenant en charge à la fois WebP et AVIF.


  1. VP8 est uniquement utilisé pour la compression avec perte. La compression sans perte utilise un autre algorithme↩︎

  2. L’implémentation dans Firefox était prévue pour Firefox 86 mais en raison du non-support des espaces de couleur, ce n’est toujours pas activé par défaut. ↩︎

  3. Le décodage progressif n’est pas prévu pour WebP mais pourrait être implémenté en utilisant des images de basse qualité pour AVIF. Voir cette question pour une discussion. ↩︎

  4. L’en-tête Vary permet de s’assurer qu’un cache intermédiaire (un proxy ou un CDN) vérifie l’en-tête Accept avant d’utiliser une réponse en cache. Internet Explorer a des difficultés avec cet en-tête et peut ne pas être en mesure de mettre la ressource en cache correctement. Il existe une solution de contournement, mais la part de marché d’Internet Explorer est désormais si faible qu’il est inutile de la mettre en œuvre. ↩︎