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.
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.
Trois détails font la différence entre une configuration correcte et une qui ne fonctionne qu’une partie du temps :
- 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. - Le Content-Type doit déclarer le format réel. Un fichier AVIF servi en
image/jpegpeut être rejeté ou mal géré par les navigateurs et les intermédiaires. - 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.
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 balisage | Oui, pour chaque image | Non |
| Qui choisit le format | Le navigateur | Le serveur, d’après Accept |
| URL par format | Distinctes | Une seule |
| Configuration du cache | Aucune précaution particulière | Vary: Accept et normalisation |
| Images déjà dans le contenu | À réécrire | Optimisées immédiatement |
| Risque principal | Balisage non mis à jour | CDN 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é
.jpgqui 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
Varysuffit à 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.