Comprendre les en-têtes de cache HTTP
La plupart des problèmes de performance que l'on tente de résoudre avec un CDN ou une couche de cache se règlent en réalité gratuitement avec trois en-têtes HTTP correctement définis, que la plupart des applications règlent mal par accident plutôt que par choix. Cache-Control, ETag et Last-Modified déterminent si un navigateur retélécharge une ressource, la revalide à peu de frais, ou la sert instantanément depuis le disque, et la différence entre ces trois issues tient entièrement à la façon dont vous les configurez.
Cache-Control : l'en-tête qui compte le plus
Cache-Control indique au navigateur (et à tout proxy ou CDN intermédiaire) combien de temps une réponse peut être réutilisée sans recontacter le serveur.
Cache-Control: max-age=3600
Cela signifie : pendant les 3600 prochaines secondes, ne demandez même pas au serveur, réutilisez la copie en cache. Aucune requête ne part durant cette fenêtre, ce qui est l'issue la plus rapide possible, plus rapide que toute optimisation côté serveur, puisqu'il n'y a aucun aller-retour.
Directives courantes :
max-age=<secondes>: durée pendant laquelle la réponse est fraîche, du point de vue d'un cache de navigateur.public: peut être mise en cache par des caches partagés (CDN, proxys), et pas seulement par le navigateur.private: seul le navigateur peut la mettre en cache ; un CDN partagé placé devant votre application ne doit pas le faire.no-cache: malgré son nom, cela autorise la mise en cache, mais impose une revalidation auprès du serveur à chaque usage (voirETagci-dessous). C'estno-storeque l'on veut pour « ne jamais mettre en cache ».no-store: ne pas mettre en cache du tout, nulle part. Le bon choix pour des réponses contenant des données sensibles propres à l'utilisateur.
Cache-Control: private, no-store # a user's account settings page
Cache-Control: public, max-age=31536000 # a hashed, versioned JS bundle
Cache-Control: no-cache # an HTML page you want revalidated every load
Pourquoi les ressources statiques doivent utiliser un max-age très long
Si votre chaîne de build produit des noms de fichiers avec un hachage de contenu (app.a3f9c1.js au lieu de app.js), le nom du fichier change dès que le contenu change. Il est donc sûr de mettre ce fichier précis en cache pour toujours, car une nouvelle version a simplement une nouvelle URL :
Cache-Control: public, max-age=31536000, immutable
immutable est un signal qui va plus loin que max-age : il indique au navigateur de ne même pas revalider cette ressource lors d'un rechargement forcé, car le contenu ne peut réellement pas changer sous cette URL. Cela n'a de sens que sur des noms de fichiers hachés et versionnés, jamais sur un simple app.js écrasé sur place, sinon chaque déploiement serait invisible pour les navigateurs déjà chargés.
Revalidation : moins coûteuse qu'un nouveau téléchargement complet
Pour les ressources qui changent mais que vous voulez malgré tout mettre en cache, max-age ne dit pas tout : vous voulez aussi que le navigateur vérifie à peu de frais « est-ce que cela a réellement changé ? » plutôt que de retélécharger aveuglément. C'est le rôle d'ETag et de Last-Modified.
app.get('/api/report/:id', async (req, res) => {
const report = await getReport(req.params.id);
const etag = `"${hashOf(report)}"`;
res.set('ETag', etag);
res.set('Cache-Control', 'no-cache');
if (req.headers['if-none-match'] === etag) {
return res.status(304).end(); // not modified — no body sent
}
res.json(report);
});
Le navigateur renvoie l'ETag qu'on lui a donné via l'en-tête If-None-Match lors de la requête suivante. S'il correspond toujours, le serveur répond 304 Not Modified sans aucun corps : le navigateur réutilise sa copie en cache. C'est pourquoi no-cache ne veut pas dire « ne pas mettre en cache » : cela veut dire « mettre en cache, mais vérifier d'abord », et cette vérification coûte bien moins cher que le renvoi de toute la réponse.
Last-Modified fonctionne de la même façon avec un horodatage au lieu d'un hachage, renvoyé sous la forme If-Modified-Since :
res.set('Last-Modified', report.updatedAt.toUTCString());
if (req.headers['if-modified-since'] === report.updatedAt.toUTCString()) {
return res.status(304).end();
}
Préférez ETag quand vous pouvez calculer un hachage peu coûteux de la réponse : il est précis (tout changement de contenu l'invalide) alors que Last-Modified n'a qu'une granularité à la seconde et dépend du suivi correct des dates de modification dans vos données.
Les empiler : une réponse d'API réaliste
Cache-Control: private, max-age=60, must-revalidate
ETag: "9f8c3a"
Cela dit : réutiliser la copie en cache pendant 60 secondes au plus sans rien demander, puis, passé cette fenêtre, revalider avec If-None-Match avant de servir soit un 304, soit des données fraîches. C'est une bonne valeur par défaut pour des réponses d'API authentifiées qui changent de temps en temps mais pas à chaque requête : le client ne martèle pas le serveur, mais n'est pas non plus coincé avec des données vieilles de plusieurs minutes.
L'erreur qui provoque les bugs « pourquoi ça ne se met pas à jour »
Définir Cache-Control: public, max-age=31536000 sur un nom de fichier simple et non haché est la cause la plus courante du « j'ai déployé un correctif et les utilisateurs voient toujours l'ancienne version ». Le navigateur n'a aucun moyen de savoir que le contenu a changé : l'URL est identique, et vous lui avez explicitement dit de ne pas demander pendant un an. La solution n'est pas un max-age plus court : c'est de donner au fichier un hachage de contenu dans son nom pour qu'un vrai changement produise une vraie nouvelle URL, et de réserver les max-age longs exclusivement aux URL qui fonctionnent ainsi.
À retenir
Cache-Control décide si le navigateur envoie même une requête. ETag/Last-Modified décident, quand il demande, si le serveur peut répondre par un 304 presque gratuit au lieu de renvoyer le corps. Utilisez un cache long et immuable uniquement sur des URL hachées selon le contenu ; utilisez un max-age court plus une revalidation pour tout ce qui peut changer sous une URL stable ; utilisez no-store pour tout ce qui est sensible et ne doit jamais être écrit dans un cache.