Self-hosting

Cache et compression Nginx pour accélérer un site

12 دقائق للقراءة

📍 Article principal du parcours : Nginx : reverse proxy, HTTPS et configuration de A à Z
Cet article fait partie du parcours Nginx. Pour la vue d’ensemble, lisez d’abord le guide principal.

Colis Express est en ligne et chiffré, mais chaque visite retélécharge le même CSS, le même JavaScript, les mêmes réponses d’API — et tout ce trafic se paie, en bande passante côté serveur et en forfait data côté visiteur. Sur une connexion mobile à Abidjan ou à Lomé, une page allégée et mise en cache, c’est la différence entre un site qui s’affiche en une seconde et un site qu’on abandonne. Dans ce tutoriel, vous activez la compression, vous dites au navigateur de garder les fichiers statiques, et vous mettez en cache certaines réponses de l’API. À la fin, votre site transférera nettement moins d’octets pour le même contenu.

🎯 Ce que vous allez apprendre

  • Activer et régler la compression gzip pour les contenus textuels.
  • Poser des en-têtes de cache navigateur sur les fichiers statiques avec expires.
  • Comprendre la différence entre cache navigateur et cache mandataire.
  • Mettre en place un cache mandataire (proxy_cache) devant l’API.
  • Vérifier les gains avec curl et les en-têtes de réponse.

🛠️ Ce que vous allez construire

Vous aurez un site dont les réponses textuelles sont compressées, dont les fichiers statiques sont gardés en cache par le navigateur pendant des mois, et dont les réponses d’API mises en cache sont servies sans solliciter Node à chaque fois. Vous saurez prouver chaque gain avec une simple commande.

Prérequis

Étape 1 — Activer la compression gzip

La compression réduit le poids des fichiers texte (HTML, CSS, JavaScript, JSON) avant de les envoyer ; le navigateur les décompresse à la réception. Les gains sont spectaculaires sur le texte — souvent 70 à 80 % de réduction — pour un coût processeur dérisoire. Réglons-la globalement dans /etc/nginx/nginx.conf, dans le bloc http.

    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_comp_level 5;
    gzip_types
        text/plain
        text/css
        application/json
        application/javascript
        text/xml
        application/xml
        image/svg+xml;

Détaillons les choix qui comptent. gzip on; active la compression. gzip_vary on; ajoute l’en-tête Vary: Accept-Encoding, indispensable pour que les caches intermédiaires distinguent version compressée et version brute. gzip_min_length 1024; évite de compresser les tout petits fichiers, pour lesquels le gain ne couvre pas le surcoût. gzip_comp_level 5; est un bon compromis : monter à 9 ne gagne presque rien tout en consommant bien plus de CPU. Enfin, gzip_types liste les types à compresser — le HTML l’est toujours par défaut, inutile de le lister. On ne compresse pas les images JPEG/PNG ni les vidéos, déjà compressées. Un point pratique : sur Debian et Ubuntu, une ligne gzip on; est déjà présente par défaut dans nginx.conf — modifiez le bloc existant plutôt que d’en ajouter un second, faute de quoi nginx -t signalera une directive en double. Validez et rechargez : sudo nginx -t && sudo systemctl reload nginx.

Point d’étapecurl -H "Accept-Encoding: gzip" -I https://colis-express.net/ renvoie un en-tête Content-Encoding: gzip. La compression est active.

Étape 2 — Mettre en cache les fichiers statiques côté navigateur

Un logo ou un fichier CSS ne change quasiment jamais entre deux visites : le retélécharger à chaque page est du gaspillage pur. L’en-tête Cache-Control dit au navigateur de garder ces fichiers localement pendant une longue durée. On cible ces ressources par leur extension dans un location dédié.

    location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|woff2)$ {
        add_header Cache-Control "public, max-age=2592000";
    }

Le location ~* \.(...)$ utilise une expression régulière insensible à la casse pour attraper les fichiers par extension. L’en-tête Cache-Control: public, max-age=2592000 autorise la mise en cache publique pendant 2 592 000 secondes, soit 30 jours. On obtiendrait un résultat équivalent avec la directive expires 30d;, qui génère seule l’en-tête Cache-Control — mais il ne faut pas combiner les deux mécanismes, sous peine d’émettre un en-tête Cache-Control en double. Résultat : lors de la deuxième visite, le navigateur ressort ces fichiers de son cache local sans même contacter le serveur. Pour gérer les mises à jour malgré ce cache long, la technique usuelle est le cache busting : on intègre un hash dans le nom du fichier (app.4f3a.js), que le build régénère à chaque changement.

Point d’étapecurl -I https://colis-express.net/app.js renvoie un en-tête Cache-Control: public, max-age=2592000. Le navigateur gardera ce fichier 30 jours.

Étape 3 — Comprendre cache navigateur et cache mandataire

Avant d’aller plus loin, distinguons deux caches très différents. Le cache navigateur (étape précédente) vit chez le visiteur : il évite de retélécharger ce qu’il a déjà. Le cache mandataire (proxy_cache) vit sur votre serveur, dans Nginx : il mémorise la réponse d’un backend pour la resservir aux visiteurs suivants sans rappeler l’application. Le premier soulage le réseau du visiteur ; le second soulage votre application et votre base de données.

Le cache mandataire ne convient qu’aux réponses identiques pour tous les utilisateurs : une liste de tarifs de livraison, des zones desservies, un catalogue public. Il ne faut jamais mettre en cache une réponse personnalisée (panier, profil, données d’un compte), sous peine de servir les données d’un client à un autre. C’est la règle d’or à garder en tête à l’étape suivante.

Étape 4 — Mettre en cache des réponses d’API

Imaginons un point d’API public et stable : /api/zones, qui renvoie la liste des quartiers desservis. Inutile de réveiller Node à chaque appel. Déclarons d’abord une zone de cache dans le bloc http de nginx.conf.

    proxy_cache_path /var/cache/nginx/colis levels=1:2
        keys_zone=colis_cache:10m max_size=200m inactive=60m;

Cette directive définit où stocker le cache sur le disque, une zone mémoire nommée colis_cache de 10 Mo pour les clés, une taille maximale de 200 Mo, et une éviction des entrées inutilisées depuis 60 minutes. Ensuite, dans le location de l’API publique concernée, on active ce cache :

    location /api/zones {
        proxy_pass http://127.0.0.1:3000/zones;
        proxy_cache colis_cache;
        proxy_cache_valid 200 10m;
        add_header X-Cache-Status $upstream_cache_status;
    }

proxy_cache colis_cache; branche la zone déclarée. proxy_cache_valid 200 10m; garde 10 minutes les réponses de code 200. La ligne add_header X-Cache-Status $upstream_cache_status; est un outil de diagnostic précieux : elle expose dans la réponse si Nginx a servi depuis le cache (HIT), interrogé le backend (MISS), ou contourné le cache (BYPASS). Rechargez après nginx -t.

Point d’étape — Deux appels successifs à /api/zones : le premier renvoie X-Cache-Status: MISS, le second HIT. Le cache mandataire fonctionne.

Étape 5 — Mesurer les gains

Optimiser sans mesurer, c’est avancer à l’aveugle. Comparons le poids transféré avec et sans compression sur une même ressource.

# Taille brute
curl -s -o /dev/null -w "%{size_download} octets\n" https://colis-express.net/app.css
# Taille compressée
curl -s -H "Accept-Encoding: gzip" -o /dev/null \
  -w "%{size_download} octets\n" https://colis-express.net/app.css

La seconde valeur doit être nettement plus basse que la première — c’est le poids réel que vos visiteurs téléchargeront. Pour une vue d’ensemble, l’onglet « Réseau » des outils de développement du navigateur affiche, colonne par colonne, la taille transférée, le statut du cache, et le temps de chargement. C’est là que se vérifie, concrètement, le travail de ce tutoriel.

Étape 6 — Aller plus loin : descripteurs de fichiers et micro-cache

Deux réglages moins connus complètent le tableau. Le premier, open_file_cache, mémorise les métadonnées des fichiers servis (leur existence, leur taille, leur date) pour éviter à Nginx d’interroger le disque à chaque requête. Sur un site qui sert beaucoup de fichiers statiques, le gain est mesurable. On le règle dans le bloc http.

    open_file_cache max=1000 inactive=20s;
    open_file_cache_valid 30s;
    open_file_cache_min_uses 2;
    open_file_cache_errors on;

Nginx garde ici en mémoire les informations de jusqu’à 1000 fichiers, oublie ceux inutilisés depuis 20 secondes, et revalide les entrées toutes les 30 secondes. C’est transparent pour le visiteur, mais cela allège le travail du serveur sous charge.

Le second concept est le micro-cache : mettre en cache une réponse dynamique pour une durée très courte, une seconde par exemple. L’idée surprend, mais elle est puissante. Si la page d’accueil de Colis Express est interrogée 200 fois par seconde lors d’un pic, la mettre en cache une seule seconde signifie que le backend ne la calcule qu’une fois au lieu de 200 — une réduction de charge spectaculaire, pour une fraîcheur que personne ne perçoit. On réutilise la zone proxy_cache déjà déclarée, avec un proxy_cache_valid très bref.

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_cache colis_cache;
        proxy_cache_valid 200 1s;
        add_header X-Cache-Status $upstream_cache_status;
    }

Cette technique ne convient qu’aux pages identiques pour tous les visiteurs anonymes — la règle d’or de l’étape 3 reste valable. Mais sur les pages publiques très sollicitées, le micro-cache est l’un des leviers les plus efficaces pour tenir un pic de trafic sans renforcer le serveur.

Point d’étape — Sous une rafale de requêtes sur la page d’accueil, X-Cache-Status affiche majoritairement HIT : le backend n’est sollicité qu’une fois par seconde.

🐞 Pièges fréquents

Symptôme / erreur Cause probable Correctif
Pas de Content-Encoding: gzip Type MIME absent de gzip_types Ajouter le type ; vérifier que le client envoie Accept-Encoding
Les mises à jour de CSS/JS ne s’affichent plus Cache navigateur trop long sans cache busting Intégrer un hash dans le nom de fichier au build
Un utilisateur voit les données d’un autre Réponse personnalisée mise en cache mandataire Ne mettre en cache que les réponses publiques et identiques pour tous
X-Cache-Status toujours MISS Le backend envoie un Cache-Control: no-cache Ajuster côté API, ou forcer avec proxy_ignore_headers
Erreur de permission sur le dossier de cache /var/cache/nginx non accessible à www-data Créer le dossier et ajuster le propriétaire

🌍 Adaptation au contexte ouest-africain

C’est sans doute le tutoriel le plus directement utile pour vos visiteurs. Sur un forfait data compté au mégaoctet, la compression gzip divise par trois ou quatre le poids des pages, ce qui se traduit en francs CFA économisés à chaque visite et en pages qui s’affichent même quand le réseau faiblit. Le cache navigateur, lui, fait qu’un visiteur régulier ne retélécharge presque rien : seul le contenu vraiment nouveau passe sur le réseau. Sur des liaisons à latence élevée, c’est souvent ce qui transforme un site « lent » en site « réactif ». Investir dans ces réglages rapporte davantage, en perception, qu’un serveur plus puissant.

✅ Récapitulatif

Vous avez activé la compression gzip, posé des en-têtes de cache navigateur sur les ressources statiques, compris la différence entre cache navigateur et cache mandataire, et mis en cache des réponses d’API publiques avec un en-tête de diagnostic. Surtout, vous savez mesurer chaque gain. Votre site est désormais rapide ; reste à le rendre capable d’encaisser la charge quand le trafic grimpe — c’est l’objet du tutoriel suivant.

🧾 Aide-mémoire

Directive Rôle
gzip on; + gzip_types Compresser les contenus textuels
expires 30d; Durée de cache navigateur des statiques
proxy_cache_path ... Déclarer une zone de cache mandataire
proxy_cache + proxy_cache_valid Mettre en cache des réponses de backend
X-Cache-Status $upstream_cache_status Diagnostiquer HIT / MISS / BYPASS

💪 À vous de jouer

Faites en sorte qu’une réponse d’API mise en cache ne soit jamais servie à un utilisateur connecté : si la requête porte un cookie de session, le cache doit être contourné. Indice : la directive proxy_cache_bypass et la variable $cookie_session.

Voir une solution
    location /api/zones {
        proxy_pass http://127.0.0.1:3000/zones;
        proxy_cache colis_cache;
        proxy_cache_valid 200 10m;
        proxy_cache_bypass $cookie_session;
        proxy_no_cache     $cookie_session;
        add_header X-Cache-Status $upstream_cache_status;
    }

proxy_cache_bypass contourne le cache (va chercher au backend) quand le cookie est présent, et proxy_no_cache empêche d’enregistrer cette réponse. Ainsi, un visiteur anonyme profite du cache, un utilisateur connecté reçoit toujours une réponse fraîche.

Tutoriels frères

Pour aller plus loin

FAQ

Faut-il préférer Brotli à gzip ?
Brotli compresse un peu mieux que gzip sur le texte, mais il n’est pas inclus dans les paquets Nginx officiels : il faut ajouter un module (souvent compilé dynamiquement). Pour débuter, gzip offre l’essentiel du gain sans cette complexité. Brotli est une optimisation de second tour.

Le cache mandataire remplace-t-il un cache applicatif type Redis ?
Non, ils sont complémentaires. Le cache Nginx mémorise des réponses HTTP entières et publiques ; un cache applicatif comme Redis stocke des données fines et personnalisables côté application. Beaucoup d’architectures utilisent les deux.

Mon site est dynamique : le cache a-t-il un intérêt ?
Oui, sur la partie statique (CSS, JS, polices, images) et sur les réponses publiques stables. Même un site très dynamique sert une majorité d’octets statiques ; la compression et le cache navigateur s’appliquent toujours.

مشاركة