Le cache HTTP : ne pas retélécharger ce qui n’a pas changé
Savoir quand une ressource est réutilisée sans aucune requête, quand elle est revalidée, et choisir les en-têtes qui évitent de servir une copie périmée.
Où en êtes-vous avec cette notion ?
Une indication personnelle, enregistrée uniquement dans ce navigateur.
À lire ensuite
Le format café
L’essentiel à comprendre, le temps d’un café.
Envie de creuser ? Un cours plus complet vous attend juste après, à déplier sans quitter cette page.
Ce que vous allez comprendre
Comment un navigateur décide de ne pas retélécharger un fichier, et pourquoi 304 Not Modified est une bonne nouvelle plutôt qu’une erreur.
Deux questions, deux mécanismes
Le cache HTTP répond à deux questions successives.
- Ma copie est-elle encore fraîche ? C’est le rôle de
Cache-Control. - Si je n’en suis pas sûr, a-t-elle changé ? C’est le rôle de la validation, avec
ETagouLast-Modified.
La fraîcheur
Cache-Control: max-age=3600
Pendant une heure, le navigateur réutilise sa copie sans contacter le serveur. Aucune requête n’est envoyée : c’est le gain le plus important.
Pour un fichier dont le nom contient une empreinte du contenu — app.a81f92.js — on peut aller beaucoup plus loin :
Cache-Control: public, max-age=31536000, immutable
Environ un an, sans risque : si le contenu change, le nom change aussi, donc l’URL change. Les deux versions ne peuvent pas être confondues.
La validation
Pour une réponse d’API, la donnée peut changer à tout moment. Le serveur joint alors une empreinte :
ETag: "abc123"
À la requête suivante, le navigateur la renvoie :
If-None-Match: "abc123"
Si rien n’a bougé, le serveur répond 304 Not Modified, sans corps. L’économie est exactement là : les en-têtes circulent, pas les 300 Ko. Sinon il répond 200 OK, avec la nouvelle version et un nouvel ETag.
Last-Modified et If-Modified-Since font le même travail avec une date. C’est plus simple à produire, mais moins précis : la granularité est la seconde, et deux modifications survenues dans la même seconde deviennent indiscernables.
no-cache n’est pas no-store
no-cache: conservez la copie, mais revalidez auprès du serveur avant de la réutiliser.no-store: ne conservez rien, nulle part.
Pour une réponse propre à un utilisateur connecté, private empêche les caches partagés — CDN, proxy inverse — de la garder pour la servir à quelqu’un d’autre.
Le cache n’est pas un seul endroit
Navigateur → CDN → proxy inverse → application
Chaque étage peut conserver une réponse. Un en-tête mal choisi ne se trompe donc pas à un seul endroit.
Fiche mémo
| En-tête | Sens |
|---|---|
max-age | durée de fraîcheur |
immutable | ne pas revalider avant expiration |
ETag / If-None-Match | validation par empreinte |
Last-Modified / If-Modified-Since | validation par date, à la seconde |
no-cache | conserver, mais revalider |
no-store | ne rien conserver |
private | caches partagés exclus |
L’idée à emporter : la requête la plus rapide est celle qu’on n’envoie pas ; la deuxième plus rapide est celle qui revient en 304, sans corps.
Et si on allait plus loin ?
Le café vous a donné les repères. Prenez maintenant le temps de comprendre les mécanismes et de pratiquer, si vous le souhaitez.
Aller plus loinChoisir entre fraîcheur et revalidation sans figer une page pour un anDévelopper le coursReplier le cours
Ce que vous saurez faire
Choisir un Cache-Control adapté à la nature de chaque ressource, mettre en place une validation par ETag, et reconnaître les réglages qui rendent un déploiement invisible chez vos visiteurs.
1. Deux états, pas un
Une réponse en cache est soit fraîche, soit périmée. Fraîche, elle est servie directement. Périmée, elle n’est pas jetée pour autant : elle peut encore servir après une revalidation réussie.
Réponse en cache
│
├── fraîche ─────────────► servie, aucune requête
│
└── périmée ──► requête conditionnelle
│
├── 304 ──► servie depuis le cache
│
└── 200 ──► nouvelle version stockée
Cette distinction explique une observation courante : l’onglet réseau montre parfois une requête pour une ressource pourtant « en cache ». C’est une revalidation. Elle coûte un aller-retour, mais pas le transfert du corps.
Référence : RFC 9111, mise en cache HTTP.
2. Poser les en-têtes selon la ressource
Contexte d’exécution : un serveur Node.js que vous administrez. Ces lignes s’insèrent dans un gestionnaire de requête, elles ne s’exécutent pas seules.
// Fichier statique dont le nom contient une empreinte du contenu.
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
// Page HTML d’entrée : elle doit pouvoir changer immédiatement.
res.setHeader('Cache-Control', 'no-cache');
// Réponse propre à l’utilisateur connecté.
res.setHeader('Cache-Control', 'private, max-age=0, must-revalidate');
// Donnée sensible qu’aucun cache ne doit conserver.
res.setHeader('Cache-Control', 'no-store');
La logique est toujours la même : plus l’URL identifie précisément un contenu figé, plus la durée peut être longue. Une URL stable dont le contenu bouge exige au contraire une revalidation.
3. Mettre en place une validation
Contexte d’exécution : le même serveur, pour une ressource JSON.
// Serveur : calcul d’une empreinte, puis réponse conditionnelle.
import { createHash } from 'node:crypto';
const corps = JSON.stringify(produit);
const etag = '"' + createHash('sha1').update(corps).digest('hex') + '"';
res.setHeader('ETag', etag);
res.setHeader('Cache-Control', 'no-cache');
if (req.headers['if-none-match'] === etag) {
res.statusCode = 304;
res.end();
} else {
res.setHeader('Content-Type', 'application/json');
res.end(corps);
}
Vérification depuis un terminal :
curl -i http://localhost:8000/api/produits/42
curl -i http://localhost:8000/api/produits/42 \
-H 'If-None-Match: "empreinte-lue-ci-dessus"'
Résultat attendu : la première commande renvoie 200 OK, le corps JSON et un en-tête ETag. La seconde, une fois l’empreinte réellement recopiée, renvoie 304 Not Modified et aucun corps.
Notez la limite de cet exemple : le serveur a quand même calculé la réponse pour en dériver l’empreinte. Une implémentation soignée construit l’ETag à partir d’une colonne de version ou d’une date de mise à jour, et évite ainsi le travail complet.
Last-Modified remplit le même rôle avec une date, le client renvoyant alors If-Modified-Since. Un serveur peut fournir les deux ; l’ETag est le plus précis.
Pièges et limites
max-agelong sur une URL stable. Unmax-age=31536000sur/app.jssans empreinte dans le nom fige la version chez les visiteurs déjà venus. Aucun moyen de rappeler ces copies : il faut changer l’URL.immutablesur une ressource qui bouge. Cette directive dit au navigateur de ne même pas revalider pendant la période de fraîcheur. À réserver strictement aux URL versionnées.- Confondre
no-cacheetno-store.no-cacheautorise le stockage et impose la revalidation. Pour un relevé bancaire, c’estno-storequ’il faut. - Oublier
privateouVary. Une réponse personnalisée conservée par un CDN peut être servie à un autre visiteur.privatel’interdit ;Varydéclare les en-têtes qui font varier la réponse. - Croire qu’un correctif d’en-tête purge le cache. Les copies déjà distribuées en amont restent en place jusqu’à leur expiration. La purge du CDN est une opération distincte.
À vous de jouer
Une application sert trois ressources depuis la même origine :
/index.html, qui référence les fichiers versionnés./assets/app.7f3c10.js, dont le nom change à chaque construction./api/moi, qui renvoie le profil de l’utilisateur connecté.
Proposez un Cache-Control pour chacune, puis dites ce qui se passerait si vous appliquiez celui de la deuxième à la première.
Correction commentée
1. /index.html → no-cache. L’URL ne change jamais, mais son contenu doit changer à chaque déploiement puisqu’il désigne les nouveaux fichiers versionnés. no-cache autorise le stockage et impose une revalidation : la plupart des visites se soldent par un 304 très léger, et un déploiement devient visible immédiatement.
2. /assets/app.7f3c10.js → public, max-age=31536000, immutable. Le contenu est figé par construction : une modification produit un autre nom, donc une autre URL. Aucune revalidation n’est utile, et public autorise un CDN à le distribuer.
3. /api/moi → private, ou no-store selon la sensibilité. private suffit si vous acceptez une copie dans le navigateur de l’utilisateur ; no-store s’impose si la réponse ne doit subsister nulle part. Dans les deux cas, un cache partagé ne doit jamais la conserver.
L’erreur à éviter. Appliquer max-age=31536000, immutable à /index.html fige la page d’entrée pendant un an chez chaque visiteur déjà venu. Ces navigateurs continueraient de réclamer les anciens fichiers versionnés, qui n’existent peut-être plus sur le serveur : l’application se retrouverait cassée sans qu’aucun déploiement ne puisse la réparer. L’URL étant stable, on ne peut pas non plus contourner le problème en la renommant.
Vous pouvez aussi vous arrêter ici. L’approfondissement est facultatif.
Les repères Cours Café
Pour aller à la source
Documentation de référence. Vérification éditoriale encore à effectuer.
Gardez une trace de cette idée.
Favoris, notes et progression seront disponibles après connexion du stockage distant.