Optimiser le poids des images avec le service conditionnel de WebP ou d’AVIF optimisés

Une même URL peut servir de l’AVIF, du WebP ou du JPEG selon ce que le navigateur prend en charge : comment fonctionne la négociation avec l’en-tête Accept, pourquoi Vary: Accept est indispensable et comment éviter de fragmenter le cache.

Une image avec la même URL /img/prodotto.jpg servie en trois formats, un AVIF de 54 KB, un WebP de 78 KB et un JPEG de 120 KB, avec l’en-tête Vary: Accept

Sur presque tous les sites, les images sont l’élément le plus lourd de la page, et très souvent le plus grand élément du premier écran est une photo : le produit, l’article, la couverture. En réduire le poids, c’est raccourcir le téléchargement de l’élément qui détermine le LCP, consommer moins de bande passante sur mobile et soulager les serveurs et les CDN. Les formats modernes, WebP et AVIF, offrent la même qualité perçue avec des fichiers bien plus légers que le JPEG. Le problème est de les servir sans exclure ceux qui ne les prennent pas en charge et sans casser les caches. Dans cet article, nous examinons le service conditionnel, c’est-à-dire une même URL qui renvoie le meilleur format pour chaque navigateur, et l’en-tête qui le rend sûr : Vary: Accept.

WebP et AVIF : combien on économise

Le WebP, introduit par Google, compresse généralement les photographies 25 à 35 % mieux que le JPEG à qualité perçue égale, et prend en charge la transparence et l’animation. L’AVIF, dérivé du codec vidéo AV1, va plus loin : sur beaucoup de photographies, il peut diviser le poids par deux par rapport au JPEG, gère mieux les dégradés et les ciels et prend en charge les gamuts de couleurs étendus. Ces chiffres sont indicatifs, car le résultat dépend du contenu de l’image et des réglages de qualité, mais la tendance est constante : à rendu égal, l’AVIF est en général le plus léger, le WebP se place au milieu, le JPEG est le plus lourd.

Tous les principaux navigateurs à jour prennent en charge le WebP, et la grande majorité prennent aussi en charge l’AVIF. Il reste pourtant des appareils et des versions obsolètes qui ne le font pas, ainsi que des clients qui ne sont pas des navigateurs : clients de messagerie, aperçus sur les réseaux sociaux, outils d’archivage. Ne servir que de l’AVIF serait risqué ; ne servir que du JPEG, c’est renoncer à l’économie. Il faut un moyen de donner à chaque client le meilleur format qu’il sait lire.

Deux voies : le balisage ou le serveur

La première voie consiste à laisser le navigateur choisir grâce au balisage, avec l’élément <picture>. On liste les versions disponibles par ordre de préférence et le navigateur utilise la première qu’il prend en charge :

<picture>
	<source type="image/avif" srcset="/img/prodotto.avif">
	<source type="image/webp" srcset="/img/prodotto.webp">
	<img src="/img/prodotto.jpg" width="1200" height="800" alt="…">
</picture>

C’est une solution robuste et transparente pour les caches, puisque chaque format a sa propre URL. Mais elle oblige à modifier le balisage de chaque image : thèmes, page builders, contenus saisis au fil des ans dans l’éditeur, images de produits générées par des plugins. Sur les sites volumineux et construits par couches, c’est souvent impraticable.

La seconde voie est le service conditionnel, aussi appelé négociation de contenu : le balisage reste le même, avec le /img/prodotto.jpg habituel, et c’est le serveur qui choisit le fichier à renvoyer d’après ce que le navigateur déclare prendre en charge. C’est l’approche qui permet d’optimiser tout un site existant sans toucher à une ligne de HTML, et c’est celle que nous approfondissons ici.

Comment fonctionne la négociation de contenu

À chaque demande d’image, le navigateur envoie l’en-tête Accept, avec la liste des formats qu’il sait afficher. Un navigateur récent déclare quelque chose comme image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8. Le serveur lit cet en-tête, vérifie s’il existe une version AVIF ou WebP de l’image demandée et renvoie la plus légère parmi celles qui sont prises en charge. L’adresse ne change pas : ce qui change, ce sont le contenu et l’en-tête Content-Type, qui déclare le format réel.

Trois navigateurs demandent la même URL /img/prodotto.jpg avec des en-têtes Accept différents : le navigateur moderne qui déclare l’AVIF reçoit un fichier image/avif d’environ 54 KB, celui qui déclare le WebP reçoit une image/webp d’environ 78 KB, l’ancien navigateur qui ne déclare pas les formats modernes reçoit une image/jpeg d’environ 120 KB
Même URL, trois réponses : chaque navigateur reçoit le format le plus léger qu’il sait lire (poids indicatifs).

Jusqu’ici, tout semble simple. Le point délicat, c’est qu’entre le navigateur et le serveur il y a presque toujours des caches : le CDN, un reverse proxy comme Varnish, parfois des proxys d’entreprise. Et un cache, par définition, conserve une réponse et la réutilise pour les demandes suivantes à la même URL.

Vary: Accept, bien expliqué

Un cache identifie chaque réponse par une clé, qui à l’origine n’est que l’URL. Si une même URL peut produire des réponses différentes selon la manière dont la requête est faite, le cache doit le savoir, faute de quoi il réutilisera pour tout le monde la première réponse reçue. L’en-tête Vary existe précisément pour cela : c’est un en-tête de réponse du serveur qui indique aux caches « ce contenu dépend aussi de ces en-têtes de la requête ». Avec Vary: Accept, le serveur dit que la réponse change selon l’en-tête Accept, et que les caches doivent l’inclure dans la clé.

Voyons ce qui se passe sans lui. Un navigateur moderne demande /img/prodotto.jpg, le serveur répond avec la version AVIF et le CDN la stocke, liée à la seule URL. Peu après arrive un navigateur qui ne prend pas en charge l’AVIF et qui demande la même URL : le CDN trouve une copie valide et la renvoie. Résultat : une image que ce navigateur ne sait pas décoder, autrement dit un cadre vide. Avec Vary: Accept, en revanche, le CDN stocke la réponse AVIF liée à la combinaison de l’URL et de l’en-tête Accept du premier navigateur ; quand le second arrive, avec un Accept différent, la clé ne correspond pas et la requête poursuit jusqu’au serveur, qui renvoie le JPEG.

Comparaison entre un cache sans et avec Vary: Accept : sans lui, le cache enregistre la version AVIF demandée par Chrome et la renvoie aussi au navigateur qui ne prend pas en charge l’AVIF, lequel affiche une image cassée ; avec Vary: Accept, la copie est enregistrée avec une clé URL plus Accept, le second navigateur a une clé différente et reçoit le bon JPEG
Vary: Accept indique aux caches qu’une même URL a plusieurs versions, et quel en-tête permet de les distinguer.

Trois détails font la différence entre une configuration correcte et une qui ne fonctionne qu’une partie du temps :

  1. Vary doit être envoyé sur toutes les variantes, JPEG compris. Si la réponse JPEG arrive sans Vary: Accept, un cache la considère valide pour tout le monde et la servira aussi aux navigateurs qui prendraient en charge l’AVIF, annulant l’économie. C’est l’erreur la plus courante.
  2. Le Content-Type doit déclarer le format réel. Un fichier AVIF servi en image/jpeg peut être rejeté ou mal géré par les navigateurs et les intermédiaires.
  3. Vary vaut aussi pour la validation. Les requêtes conditionnelles avec ETag, que nous avons vues dans l’article sur le code de statut 304, doivent se rapporter à la bonne variante : chaque format doit être traité comme une ressource distincte, avec son propre identifiant de version.

Revers de la médaille : la fragmentation du cache

Vary: Accept résout le problème de la justesse, mais en crée un d’efficacité. L’en-tête Accept n’est pas le même pour tout le monde : il change selon les navigateurs, selon les versions d’un même navigateur, selon les types de requêtes. Un cache qui utilise la valeur complète comme partie de la clé finit par stocker des dizaines de copies de la même image, une pour chaque variante de l’en-tête, alors que les fichiers réellement différents ne sont que trois. Chaque nouvelle combinaison est un MISS, le taux de HIT s’effondre et l’origine reçoit beaucoup plus de requêtes que nécessaire.

La solution est de normaliser l’en-tête avant qu’il ne devienne une clé de cache : réduire les dizaines de valeurs possibles à seulement trois, « avif », « webp » et « jpeg », d’après ce que le client prend réellement en charge. Ainsi, chaque image a au plus trois copies dans le cache, une par format.

Sept en-têtes Accept différents envoyés par des navigateurs et des versions différents convergent vers seulement trois clés de cache : avif pour ceux qui déclarent l’AVIF, webp pour ceux qui ne déclarent que le WebP, jpeg pour tous les autres
Normaliser Accept ramène les variantes en cache de dizaines à trois, et le taux de HIT reste élevé.

Dans Varnish, la normalisation se fait à l’arrivée de la requête, en réécrivant l’en-tête avant qu’il serve à la recherche dans le cache. Varnish gère Vary nativement ; il ne comparera donc plus que trois valeurs possibles :

sub vcl_recv {
	if (req.url ~ "\.(jpe?g|png)(\?.*)?$") {
		if (req.http.Accept ~ "image/avif") {
			set req.http.Accept = "image/avif";
		} elseif (req.http.Accept ~ "image/webp") {
			set req.http.Accept = "image/webp";
		} else {
			set req.http.Accept = "image/jpeg";
		}
	}
}

Avec les CDN, il faut vérifier au cas par cas. Certains ignorent Vary ou ne l’acceptent que pour quelques valeurs, d’autres proposent des réglages dédiés aux images qui gèrent en interne le format et les clés de cache, d’autres encore exigent d’inclure explicitement l’en-tête normalisé dans la clé. Avant d’activer le service conditionnel derrière un CDN, il vaut la peine de lire comment il traite Vary et de vérifier le comportement avec de vraies requêtes : c’est le point où quelque chose se casse le plus souvent, comme nous l’avons vu en parlant de Cloudflare et du cache.

Un exemple avec Nginx

Une façon simple de mettre en œuvre le service conditionnel est de générer à l’avance les versions modernes, à côté de chaque image, avec un suffixe : prodotto.jpg.avif et prodotto.jpg.webp. Nginx choisit le suffixe selon Accept et sert le premier fichier existant, en se rabattant sur l’original :

map $http_accept $img_suffix {
	default         "";
	"~*image/avif"  ".avif";
	"~*image/webp"  ".webp";
}

location ~* \.(?:jpe?g|png)$ {
	add_header Vary Accept always;
	try_files $uri$img_suffix $uri =404;
}

# Si elles sont absentes du mime.types de la version utilisée :
types {
	image/avif avif;
	image/webp webp;
}

Les expressions régulières du map sont évaluées dans l’ordre où elles sont écrites ; l’AVIF, s’il est déclaré, a donc la priorité sur le WebP. L’en-tête Vary est ajouté à toutes les réponses du bloc, JPEG compris, et le Content-Type suit l’extension du fichier réellement servi. On peut construire une configuration équivalente avec Apache et mod_rewrite, en contrôlant l’en-tête Accept et l’existence du fichier.

Comment générer les variantes

Les versions WebP et AVIF peuvent être créées au téléversement des images, par un plugin ou une procédure du CMS ; à la compilation (build), pour les sites statiques ou les thèmes ; ou à la volée par un service d’optimisation d’images, qui les conserve ensuite en cache. La règle importante est de ne pas les encoder à chaque requête : la compression AVIF en particulier est bien plus lente que celle du JPEG, et la faire en temps réel sans cache reporterait le coût sur le TTFB des images.

La qualité doit être calibrée à l’œil, en comparant les versions sur les images typiques du site : les meilleures valeurs pour une photo de produit sur fond blanc ne sont pas les mêmes que pour un paysage ou une capture d’écran avec du texte. Et le format ne remplace pas les dimensions : un AVIF de 2 000 pixels servi à un smartphone reste du gaspillage. Le service conditionnel choisit le format, tandis que srcset et sizes continuent de choisir la bonne taille pour l’écran.

Picture ou service conditionnel ?

<picture>Service conditionnel
Modification du balisageOui, pour chaque imageNon
Qui choisit le formatLe navigateurLe serveur, d’après Accept
URL par formatDistinctesUne seule
Configuration du cacheAucune précaution particulièreVary: Accept et normalisation
Images déjà dans le contenuÀ réécrireOptimisées immédiatement
Risque principalBalisage non mis à jourCDN qui ignorent Vary

Les deux voies peuvent aussi coexister : <picture> pour les images contrôlées par le thème, comme la couverture ou l’image principale, et le service conditionnel pour tout ce qui vient du contenu.

Les pièges à connaître

  • Fichiers enregistrés avec la mauvaise extension. Celui qui enregistre une image depuis le navigateur peut se retrouver avec un fichier nommé .jpg qui contient en réalité de l’AVIF, que certains programmes n’ouvriront pas.
  • Clients qui ne déclarent pas de formats. Les aperçus sociaux, les clients de messagerie et les outils automatisés envoient souvent Accept: */* : ils doivent recevoir le JPEG, ce que fait précisément une configuration correcte.
  • Vary oublié sur une variante. Une seule réponse sans Vary suffit à contaminer un cache partagé.
  • Clé de cache avec l’Accept complet. Sans normalisation, le taux de HIT des images s’effondre et l’origine finit par servir beaucoup plus de trafic.
  • Encodage à la volée sans cache. Convertir en AVIF à chaque requête allège les images mais les fait arriver plus lentement.

Comment vérifier que ça fonctionne

Le contrôle le plus direct consiste à simuler différents navigateurs en modifiant l’en-tête Accept et en observant Content-Type, Vary et, s’il est présent, l’état du cache du CDN :

# Navigateur qui prend en charge l’AVIF
curl -sI -H "Accept: image/avif,image/webp,*/*" https://www.votresite.fr/img/prodotto.jpg \
  | grep -iE "content-type|vary|content-length"

# Client qui ne déclare pas les formats modernes : il doit recevoir le JPEG
curl -sI -H "Accept: */*" https://www.votresite.fr/img/prodotto.jpg \
  | grep -iE "content-type|vary|content-length"

En répétant les requêtes, on vérifie que le cache renvoie chaque fois la bonne variante. Dans le panneau Réseau des DevTools, la colonne du type indique le format reçu et la colonne de la taille le poids réel ; dans les outils de laboratoire, l’audit des formats d’image modernes doit disparaître. Dans les données d’utilisateurs réels, l’effet se voit sur le LCP, surtout sur mobile.

En résumé

Le WebP et l’AVIF réduisent de façon constante le poids des images, et avec lui le temps de chargement de l’élément principal de la page. Le service conditionnel permet de les adopter sur un site existant sans changer le balisage : la même URL renvoie le meilleur format que chaque navigateur déclare prendre en charge par l’en-tête Accept.

La clé pour le faire en toute sécurité est Vary: Accept, présent sur chaque variante, qui empêche les caches de servir un format à quelqu’un qui ne le prend pas en charge. La clé pour le faire efficacement est de normaliser Accept sur trois valeurs seulement, afin de ne pas fragmenter le cache. Avec des variantes générées à l’avance, des CDN vérifiés et des tailles adaptatives, le résultat est le même site, avec des images identiques aux yeux des utilisateurs et des dizaines de kilo-octets en moins sur chaque page.

L’ÉTAPE SUIVANTE

Commençons
par votre site.

Parlez-nous du projet, des problèmes que vous constatez et de vos objectifs. Ensemble, nous définirons une intervention sur mesure.

Demander une analyse

Vous mène au formulaire de la page Contact.

MANAGED SERVER S.R.L.info@corewebvitals.fr +39 02 5656 9681

Du lundi au vendredi · 9 h 30 – 19 h 30 (CET)

Vous préférez un formulaire en ligne ?
Contactez-nous sur managedserver.it ↗