Comment convertir vos images en WebP et les afficher avec Nginx

Dans ce tutoriel, nous allons voir comment convertir vos images en WebP, sans avoir recours à des plugins dans vos CMS, et comment configurer Nginx pour servir automatiquement la version WebP d'une image à condition qu'elle existe et que le format soit supporté par le navigateur du visiteur.

WebP est un format d'image open-source développé par Google en 2010, basé sur un autre format de fichiers également développé par Google, le VP8.

Le format WebP par rapport aux autres formats

Selon Google, le niveau de compression du format WebP permet d'obtenir des images 26% plus légères que le PNG et jusqu'à 34% plus légères que le JPG.  Il faut savoir que le WebP supporte la transparence (canal alpha), mais également les animations (GIF).

Cependant, les chiffres annoncés par Google n'ont pas convaincu tout le monde. C'est notamment le cas de la fondation Mozilla, qui estime que les gains du WebP en compression ne sont pas aussi élevés et qui remet surtout en cause les protocoles de test utilisés par Google pour obtenir ces résultats.

Mise à jour du 29/01/2019 : Mozilla FireFox supporte officiellement les images au format WebP avec la mise à jour publiée aujourd'hui (version 65.0).  Les seuls navigateurs modernes qui ne supportent pas le format WebP sont Edge et Safari.

Ce qui nous amène à un point qu'il est important  de souligner : tous les navigateurs web ne supportent pas le WebP. Ce qui induit qu'il est actuellement impossible d'utiliser uniquement des images au format WebP sur un site (du moins, pas sans impacter certains visiteurs) et qu'il est nécessaire de configurer son serveur web pour servir la version WebP d'une image, seulement si le navigateur du visiteur le supporte.

Compatibilité et gains

On peut alors se poser la question, cela vaut-il le coût de supporter le format WebP pour un site web ? Pour y répondre, il faudrait savoir si la majorité des internautes utilise un navigateur qui supporte le WebP. Il est heureusement assez facile d'obtenir la réponse, grâce au site caniuse.com, dont voici les résultats :

Can I Use webp?

En se basant sur les parts de marché de chaque navigateur, le site estime que 73% des internautes utilisent un navigateur qui supporte le WebP. Ce qui semble tout à fait plausible étant donné la popularité du navigateur Google Chrome.

Et le niveau compression dans tout ça ? Est ce que le format WebP permet vraiment d'obtenir des images plus légères ?

D'après les tests que j'ai réalisé sur mes sites, la réponse est OUI. Qu'il s'agisse de convertir une image au format JPEG ou au format PNG, la version WebP est plus légère même s'il ne s'agit parfois que de quelques kilo-octets. Quelques exemples sur les images du blog :

| Image | Original | WebP |
| -------- | -------- | -------- | -------- |
| MariaDB-10.3-1.jpg | 52Ko | 32Ko |
| background-869581.png | 52Ko | 36Ko |
| cloudflare-origin.png | 492Ko | 344Ko |
| library-898333_1920-1.jpg | 348Ko | 228Ko |

La conversion a été effectuée en mode lossless pour les images PNG , avec l'argument (-z 9) et avec un indice de qualité de 82 pour les images JPEG, avec l'argument (-q 82).

Installer cwebp pour convertir les images

Cwebp est l'utilitaire en ligne de commande qui permet de convertir les images au format WebP. Cwebp est inclus dans le paquet webp disponible depuis les dépôts APT ou YUM, mais également dans le paquet libwebp qu'il est possible de compiler depuis les sources.

Depuis les dépôts APT ou YUM

Sous Debian/Ubuntu, vous pouvez installer cwebp avec :

sudo apt install webp -y

Sous Centos 7, vous pouvez installer cwebp avec :

sudo yum install libwebp-tools -y

Depuis les sources de libwebp

Si vous souhaitez installer la dernière version du paquet cwebp (actuellement en v1.0.2) plutôt qu'utiliser la version disponible sur les dépôts APT et YUM (version 0.6.0 sur Ubuntu 18.04 LTS), vous pouvez télécharger les sources de libwebp depuis le dépôt Google

Mise à jour du 17/08/2019 : Je propose désormais un script pour automatiquement compiler la dernière version de libwebp :

curl -sL git.io/fjdd6 | sudo -E bash

Le script est disponible sur Github : https://github.com/VirtuBox/img-optimize/blob/master/scripts/install-webp.sh

Pour cela, il faut tout d'abord installer les pré-requis à la compilation :

sudo apt-get install build-essential libjpeg-dev libpng-dev libtiff-dev libgif-dev libwebp-dev -y

Vous pouvez ensuite télécharger les sources de libwebp et les extraire

curl -sL https://storage.googleapis.com/downloads.webmproject.org/releases/webp/libwebp-1.0.2.tar.gz | tar zxf - -C /usr/local/src
cd /usr/local/src/libwebp-*

Il ne reste plus qu'à configurer libwebp avant la compilation et l'installation

./configure --prefix=/usr
make -j "$(nproc)"
sudo make install

Vous pouvez ensuite vérifier si vous utilisez la version compilé de cwebp avec la commande

which cwebp

Qui vous retournera normalement /usr/local/bin/cwebp

La conversion des images en WebP

Convertir une image en WebP avec cwebp

La conversion d'une image avec cwebp est on ne peut plus simple puisqu'il suffit de définir l'image à convertir, suivie de l'argument -o permettant de définir le nom du fichier en sortie.

Exemple :

cwebp monimage.png -o monimage.webp

Il est ensuite possible de d'ajouter des paramètres de conversion en passant par des arguments après la commande cwebp

Voici les paramètres principaux à utiliser en arguments avec cwebp :

  • -lossless : pour encoder les images sans pertes (PNG)
  • -q : Est un indice de qualité, similaire au niveau de qualité des images JPEG.
  • -z : Est un indice de compression en mode lossless, allant de 0 à 9, où 9 est le niveau le plus haut, cela nécessite un temps de conversion plus long mais permet d'obtenir un fichier plus léger en sortie
  • -mt : utiliser le multi-threading si possible

Ces paramètres sont très important pour la conversion en WebP, car l'utilisation d'un argument inadapté au type d'image peut rendre le fichier WebP plus lourd que le fichier original.

Pour les images JPEG, il faut privilégier l'usage de l'indice de qualité -q.
Pour les images PNG, le plus simple est d'utiliser -z pour définir le niveau de compression en mode lossless.

La liste complète des options est disponible sur le site developers.google.com.

Convertir toutes les images de votre site en WebP

Après avoir vu comment convertir une image au format WebP en utilisant cwebp, nous allons aller un peu plus loin en appliquant cette méthode pour convertir toutes les images d'un site web.

Pour cela, il nous faut tout d'abord lister les images au format JPEG et PNG présentes dans le répertoire images de notre site, avant de les convertir au format WebP. L'objectif du tutoriel étant de pouvoir servir les images WebP avec Nginx, il est important de définir la structure que nous allons utiliser pour nommer les images WebP par rapport à la version originale de l'image.

Voici la structure que nous allons utiliser pour nommer les fichiers WebP :

# une image png
monimage.png -> monimage.png.webp

# une image jpg
monimage.jpg -> monimage.jpg.webp

Pourquoi nommer les images converties ainsi ?

Car cette structure va nous permettre d'utiliser la directive Nginx `try_files`. Ainsi Nginx pourra vérifier l'existence du fichier WebP par rapport à l'image originale.

Lister les images PNG & JPG dans un répertoire

Pour obtenir récursivement, la liste des images PNG et JPG présentes dans un répertoire, nous allons utiliser la commande find. Petite particularité pour les images au format JPEG, l'extension du fichier peut-être .jpg ou .jpeg.

On se rend dans le répertoire qui contient les images

cd /votre/répertoire/images

lister les images JPEG

find . -type f \( -iname "*.jpg" -o -iname "*.jpeg" \)

lister les images PNG

find . -type f -iname '*.png'

Convertir les images en WebP conditionnellement

Maintenant que nous sommes capables de lister les images PNG et JPG dans un répertoire, nous souhaitons pouvoir les convertir en WebP. Cependant nous ne voulons pas convertir une seconde fois des images déjà converties en WebP.

Nous allons donc procéder en deux étapes :

  1. lister les images PNG & JPG
  2. si la version WebP des images n'existe pas, les convertir en WebP

Ce qui nous donne  :

Convertir les images PNG

find . -type f -iname "*.png" -print0 | xargs -0 -I {} \
bash -c '[ ! -f "{}.webp" ] && { cwebp -z 9 -mt {} -o {}.webp; }'

convertir les images JPG

find . -type f \( -iname "*.jpg" -o -iname "*.jpeg" \) -print0 | xargs -0 -I {} \
bash -c '[ ! -f "{}.webp" ] && { cwebp -q 82 -mt {} -o {}.webp; }'

Détaillons un peu notre exemple :

On utilise tout d'abord find pour obtenir la liste des images PNG ou JPG dans un répertoire, suivi de l'argument -print0 qui permet de transmettre le nom des fichiers à xargs même s'ils contiennent des caractères spéciaux

On utilise ensuite un pipe | pour connecter cette première commande et récupérer sa sortie standard avec xargs.

Grâce à xargs -0 nous sommes capables de récupérer les résultats obtenu avec find et de les passer en tant qu'argument à la seconde partie de la commande.

Enfin on utilise bash -c pour invoquer un shell et traiter notre conversion conditionnelle en WebP. Si vous souhaitez en savoir plus, je vous conseille de lire l'article de Wikipédia sur xargs.

Dans le shell invoqué, on créer la condition "si le fichier monimage.jpg.webp n'existe pas, alors utiliser cwebp" :

[ ! -f "{}.webp" ] && { cwebp -q 82 -mt {} -o {}.webp; }

Img-optimize - Un Script pour optimiser et convertir vos images en Webp

Malgré que la conversion des images en WebP ne soit pas réellement difficile avec cwebp, j'ai publié img-optimize sur Github, un script bash qui permet de réaliser la conversion de vos images en WebP mais également l'optimisation des images au format JPG et PNG, en ajoutant simplement le répertoire qui contient les images en argument.

Pour utiliser le script il vous suffit de clone le dépot :

git clone https://github.com/VirtuBox/img-optimize.git $HOME/.img-optimize

Puis d'ajouter un alias via le fichier .bashrc

echo "alias img-optimize=$HOME/.img-optimize/optimize.sh" >> $HOME/.bashrc
source $HOME/.bashrc

Vous pouvez ensuite optimiser et convertir les images présentes dans un répertoire avec :

img-optimize /dossier/des/images

Le script supporte également les arguments suivants :

Bash script to optimize your images and convert them in WebP
Usage: img-optimize [options] <images path>
If images path isn't defined, img-optimize will use the current directory
  Options:
       --jpg <images path> ..... optimize all jpg images
       --png <images path> ..... optimize all png images
       --webp <images path> ..... convert all images in webp
       --nowebp <images path> ..... optimize all png & jpg images
       --all <images path> ..... optimize all images (png + jpg + webp)
       -i, --interactive ..... run img-optimize in interactive mode
       -q, --quiet ..... run image optimization quietly
 Other options :
       -h, --help, help ... displays this help information
Examples:
  optimize all jpg images in /var/www/images
    img-optimize --jpg /var/www/images

Afficher les images WebP avec Nginx

Il nous faut ensuite configurer Nginx pour afficher les images en WebP, mais avec plusieurs conditions :

  1. Il faut que la navigateur du visiteur supporte le format WebP
  2. Il faut que la version WebP de l'image existe

Détecter si un navigateur supporte le format WebP

Pour la première condition, il est assez facile de détecter si un navigateur supporte le WebP avec Nginx, puisque cette informations est transmise par la navigateur via les "Requests Headers".
Sur un navigateur compatible comme Google Chrome, on y retrouve la ligne :

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8

Où l'on retrouve bien image/webp. Contrairement à un navigateur comme Firefox qui ne supporte pas le format WebP :

Accept : text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8

Définir l'extension à utiliser avec la directive map

La directive map de Nginx est particulièrement intéressante, puisqu'elle permet d'associer des valeurs à plusieurs variables, sans avoir recours au if.

Dans notre cas, nous allons associer la variable $http_accept qui fait partie des "Requests Headers" avec l'extension de nos images, variable que j'ai nommé $webp_suffix.
Nous allons créer un fichier webp.conf dans /etc/nginx/conf.d dans lequel nous allons placer notre directive map :

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

On définit ici la valeur de la variable $webp_suffix en fonction du contenu de la variable $http_accept. Par défault, $webp_suffix est nulle, mais lorsque $http_accept contient le mot "webp", alors $webp_suffix est égale à ".webp".

Servir sous conditions les versions WebP des images

Pour servir une image au format WebP, les deux conditions sont :

  • que le navigateur accepte les format WebP
  • que la version WebP de l'image existe

Pour cela nous allons utiliser notre variable $webp_suffix associée à la directive try_files qui permet à Nginx de vérifier si un fichier existe.

On commence donc par créer un bloc location pour sélectionner les images PNG & JPG :

location ~ \.(png|jpe?g)$ {}

Dans ce bloc, nous allons ajouter plusieurs directives :

  • add_header Vary "Accept-Encoding"; : permet d'annoncer qu'il peut exister plusieurs versions du même fichier selon les Requests Headers transmis.
  • add_header "Access-Control-Allow-Origin" "*"; : active les Cross-Origin Resource Sharing (CORS)
  • try_files $uri$webp_suffix $uri =404; : demande à Nginx d'essayer l'url de l'image suivi de notre variable $webp_suffix, et de vérifier que le fichier existe bien.

On obtient alors :

location ~ \.(png|jpe?g)$ {
    add_header Vary "Accept-Encoding";
    add_header "Access-Control-Allow-Origin" "*";
    try_files $uri$webp_suffix $uri =404;
}

Auquel, on peut ajouter quelques options supplémentaires :

  • add_header Cache-Control "public, no-transform"; : pour définir le cache comme public et demander aux éventuels proxy de ne pas le modifier
  • access_log off; : pour désactiver les logs d'accès
  • log_not_found off; : désactive les logs d'erreurs en cas de fichier introuvable
  • expires max; : définit la durée autorisée de cache par les navigateurs, le plus longtemps possible puisqu'il s'agit d'images.

Le dernier point important, est de bien définir le répertoire dans lequel nous allons demander à Nginx de servir des images WebP, plutôt que d'appliquer cela à l'ensemble d'un site.

Voici un exemple complet à inclure dans un vhost pour WordPress :

location /wp-content/uploads/ {
   location ~ \.(png|jpe?g)$ {
       add_header Vary "Accept-Encoding";
       add_header "Access-Control-Allow-Origin" "*";
       add_header Cache-Control "public, no-transform";
       access_log off;
       log_not_found off;
       expires max;
       try_files $uri$webp_suffix $uri =404;
   }
}

Et un autre exemple pour Ghost :

location /content/images {
    alias /var/www/votredomaine.tld/content/images;
    location ~ \.(png|jpe?g)$ {
        add_header Vary "Accept-Encoding";
        add_header "Access-Control-Allow-Origin" "*";
        add_header Cache-Control "public, no-transform";
        access_log off;
        log_not_found off;
        expires max;
        try_files $uri$webp_suffix $uri =404;
    }
}

Le script conversion des images en WebP est inspiré de l'article How To Create and Serve WebP Images to Speed Up Your Website publié sur DigitalOcean Community