Le cache HTTP est le levier de performance le moins cher d’un site web : bien réglé, il évite des requêtes entières et sert des fichiers depuis un point proche de l’internaute. Mal réglé, il livre du contenu périmé ou, à l’inverse, ne cache rien du tout. Trois mécanismes suffisent à couvrir la quasi-totalité des besoins : l’en-tête Cache-Control, la revalidation par validateurs, et le versionnage des URL. Ce guide les relie en une stratégie cohérente, du navigateur au CDN.
Deux familles de cache, un seul en-tête pivot
Un cache HTTP se range dans l’une de deux catégories. Le cache privé est celui du navigateur, propre à un internaute. Le cache partagé est celui que traversent tous les visiteurs : un CDN, un reverse proxy comme Varnish, ou le cache d’un serveur nginx. La distinction est décisive car une réponse personnalisée ne doit jamais atterrir dans un cache partagé, où elle serait resservie à un autre visiteur.
L’en-tête qui pilote tout cela est Cache-Control, présent dans la réponse du serveur. Il remplace les anciens Expires et Pragma, qu’il n’est plus utile d’émettre. Sa lecture se fait directive par directive, et quelques-unes suffisent à décrire la plupart des politiques. Sur un serveur auto-hébergé, ces réglages se posent une fois pour toutes dans la configuration du serveur web, comme le décrit le guide sur l’auto-hébergement de WordPress avec CloudPanel.
Les directives Cache-Control qui comptent
| Directive | Effet | Usage typique |
|---|---|---|
max-age=N | Réponse fraîche pendant N secondes | Durée de vie par défaut |
s-maxage=N | Comme max-age, mais pour les caches partagés uniquement | CDN plus agressif que le navigateur |
public | Réponse stockable par un cache partagé | Fichiers statiques |
private | Stockage réservé au navigateur | Page personnalisée |
no-cache | Stockable, mais à revalider avant chaque usage | Document HTML qui change souvent |
no-store | Interdiction totale de stocker | Données sensibles |
must-revalidate | Une fois périmée, la réponse ne peut plus être servie sans revalidation | Contenu à exactitude critique |
immutable | Pas de revalidation pendant la fraîcheur, même sur rechargement | Fichiers à URL versionnée |
stale-while-revalidate=N | Sert la version périmée jusqu’à N s pendant qu’une nouvelle se charge en arrière-plan | Réponses tolérant un léger décalage |
La confusion la plus fréquente oppose no-cache et no-store. La première autorise le stockage mais impose une revalidation systématique ; la seconde interdit tout stockage. Pour un tableau de bord affichant des données confidentielles, seul no-store convient.
La revalidation : ETag et Last-Modified
Quand une réponse cachée arrive à expiration, le cache n’a pas besoin de la retélécharger entièrement : il demande au serveur si elle a changé. Ce dialogue repose sur deux validateurs. Le serveur émet un ETag, empreinte opaque du contenu, ou un Last-Modified, date de dernière modification. Au tour suivant, le cache renvoie ces valeurs dans If-None-Match ou If-Modified-Since.
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: max-age=0, must-revalidate
ETag: "9c1e-5f3a2b"
# Requête suivante, une fois la réponse périmée :
GET /api/profil HTTP/1.1
If-None-Match: "9c1e-5f3a2b"
# Le serveur répond sans corps si rien n'a changé :
HTTP/1.1 304 Not Modified
ETag: "9c1e-5f3a2b"La réponse 304 Not Modified ne transporte aucun corps : seuls les en-têtes circulent. Sur une ressource volumineuse, l’économie de bande passante est majeure, et la latence perçue chute puisque le navigateur réutilise sa copie locale. Un ETag se décline en version forte ou faible, cette dernière préfixée de W/ lorsqu’une équivalence sémantique suffit, sans exiger une identité octet pour octet.
Un cache bien réglé ne se mesure pas au nombre de fichiers stockés, mais au nombre de requêtes qu’il n’a jamais eu à traiter.
Cache partagé, cache privé et l’en-tête Vary
Une même URL peut renvoyer des variantes : une version compressée en gzip ou en brotli, une traduction selon la langue. L’en-tête Vary indique au cache partagé quels en-têtes de requête distinguent ces variantes. Sans lui, un CDN risque de servir la version anglaise à un visiteur francophone, ou du contenu compressé à un client qui ne sait pas le décompresser.
# Fichiers statiques versionnés : cache long et immuable
location ~* \.(?:css|js|woff2|png|jpg|svg)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
}
# Document HTML : toujours revalidé
location / {
add_header Cache-Control "no-cache";
add_header Vary "Accept-Encoding";
}La règle générale tient en une phrase : cache long et immuable pour les fichiers dont le nom porte une empreinte, revalidation systématique pour le HTML qui les référence. Ce découpage sépare proprement ce qui change de ce qui ne change jamais. Il vaut aussi pour la sécurité, car une réponse authentifiée mal typée qui fuite dans un cache partagé est une faille : le socle décrit dans le durcissement minimal de WordPress inclut cette vérification.
Invalidation : versionner l’URL plutôt que purger
Le problème réputé difficile du cache est l’invalidation. Purger un CDN à chaque déploiement est lent et faillible. La technique robuste consiste à changer l’URL du fichier quand son contenu change, en y intégrant une empreinte : app.9f3c1a.js plutôt que app.js. Le fichier devient alors éligible à un max-age d’un an et à immutable, puisqu’une nouvelle version portera un nom différent. Les bundlers modernes génèrent ces empreintes automatiquement, ce qui rend la stratégie quasi gratuite à mettre en place.
Le seul document qui ne peut pas être versionné est le point d’entrée HTML, dont l’URL est publique et stable. Lui reste donc en no-cache, revalidé à chaque visite : il pèse peu et référence les fichiers empreintés. L’effet de cette politique se lit directement dans les métriques de performance, que l’on peut suivre via GA4 et la Search Console.
Vérifier ces réglages en production prend quelques secondes : une requête curl -I sur un fichier statique doit renvoyer un Cache-Control long et immuable, la même requête sur le HTML un no-cache. Côté CDN, le rapport entre réponses servies depuis le cache et réponses transmises à l’origine, souvent exposé par un en-tête de diagnostic comme X-Cache, mesure l’efficacité réelle de la politique bien mieux que le nombre de fichiers stockés. Un taux de succès qui stagne trahit presque toujours un en-tête mal posé plutôt qu’un cache insuffisant.
Trois pièges reviennent en production. Poser un max-age long sur un fichier HTML non versionné fige le site pour les visiteurs jusqu’à expiration. Oublier Vary: Accept-Encoding derrière un CDN provoque des réponses illisibles. Enfin, laisser un cache partagé stocker une réponse marquée private par erreur expose des données d’un internaute à un autre : la revue de ces trois points avant mise en ligne évite l’essentiel des incidents.
Ce qu’il faut retenir
La stratégie tient en trois décisions. Choisir la durée de fraîcheur avec max-age et s-maxage selon le type de cache visé. Activer la revalidation par ETag pour transformer les rechargements en réponses 304 sans corps. Versionner les fichiers statiques pour combiner cache d’un an et invalidation immédiate. Le HTML reste revalidé, les fichiers empreintés sont figés, et Vary protège les variantes. Cette combinaison couvre la grande majorité des sites sans configuration exotique.
Sur les sites que j’ai repris, la panne de performance la plus courante n’était pas l’absence de cache mais un cache mal ciblé : du HTML figé pour une journée, des fichiers statiques revalidés à chaque page. J’applique désormais une règle unique dès la mise en production, avant même d’optimiser le reste : HTML en no-cache, statiques empreintés en immutable. C’est le réglage qui rapporte le plus pour le moins d’effort, et je n’ai jamais eu à le regretter. — Simon Janvier
Pour aller plus loin : la référence MDN sur le cache HTTP et la spécification RFC 9111.
