Pourquoi la compression HTTP reste essentielle
Quand une page web semble lente, on pense souvent au JavaScript, aux images ou au rendu côté navigateur. Ces sujets sont importants, comme on l’a vu avec l’optimisation des images dans Optimiser les images web sans sacrifier la qualité, mais il ne faut pas oublier une couche plus discrète : le transport HTTP.
La compression HTTP consiste à réduire la taille des ressources textuelles avant de les envoyer au navigateur. HTML, CSS, JavaScript, JSON, SVG, polices et fichiers manifest peuvent souvent être compressés fortement. Un fichier JavaScript de 300 Ko peut parfois descendre sous les 90 Ko avec Brotli, selon son contenu.
Ce gain a un impact direct sur le temps de chargement, surtout sur mobile ou sur des connexions instables. Moins d’octets transférés signifie moins d’attente réseau, une meilleure perception de rapidité et, souvent, de meilleurs signaux de performance pour le SEO.
Comprendre Accept-Encoding et Content-Encoding
La compression HTTP repose sur une négociation simple entre le navigateur et le serveur.
Le navigateur envoie un en-tête Accept-Encoding pour annoncer les formats qu’il comprend :
Accept-Encoding: br, gzip, deflate, zstd
Le serveur choisit ensuite un format compatible, compresse la réponse et indique son choix avec Content-Encoding :
Content-Encoding: br
Vary: Accept-Encoding
L’en-tête Vary: Accept-Encoding est crucial. Il indique aux caches intermédiaires qu’une même URL peut produire plusieurs variantes selon les formats de compression acceptés par le client. Sans lui, un proxy pourrait servir une réponse compressée dans un format non supporté par certains navigateurs.
Brotli, gzip, deflate : que choisir ?
En pratique, deux formats dominent encore largement côté web : gzip et Brotli.
gzip : le socle compatible
gzip est ancien, très bien supporté et encore indispensable comme fallback. Il est rapide à compresser et à décompresser, ce qui le rend adapté à beaucoup de configurations serveur.
Il reste un excellent choix pour les réponses dynamiques, notamment les API JSON, à condition de ne pas compresser des réponses minuscules ou déjà compressées.
Brotli : souvent meilleur pour les fichiers statiques
Brotli produit généralement de meilleurs ratios de compression que gzip sur les ressources textuelles web, en particulier CSS, JavaScript, HTML et SVG. Il est très pertinent pour les fichiers statiques générés au build : bundles, feuilles de style, chunks, polices textuelles et documents statiques.
La contrepartie est que Brotli peut être coûteux à compresser avec les niveaux les plus élevés. C’est pourquoi on préfère souvent précompresser les fichiers pendant le build plutôt que les compresser à la volée à chaque requête.
Éviter la compression inutile
Certains fichiers sont déjà compressés : JPEG, PNG, WebP, AVIF, MP4, PDF, ZIP, WOFF2. Les recompresser consomme du CPU pour un gain négligeable, parfois négatif.
Une règle saine consiste à compresser surtout les types textuels :
text/html
text/css
text/javascript
application/javascript
application/json
application/ld+json
image/svg+xml
text/plain
application/xml
Configuration avec Nginx
Nginx peut gérer gzip directement. Brotli nécessite souvent un module additionnel selon votre distribution ou votre image Docker.
Voici une configuration gzip raisonnable :
gzip on;
gzip_comp_level 6;
gzip_min_length 1024;
gzip_vary on;
gzip_proxied any;
gzip_types
text/plain
text/css
text/javascript
application/javascript
application/json
application/ld+json
application/xml
image/svg+xml;
Quelques points importants :
gzip_min_length 1024évite de compresser de très petites réponses ;gzip_vary onajouteVary: Accept-Encoding;gzip_comp_level 6offre souvent un bon compromis entre ratio et CPU.
Pour Brotli, selon le module disponible, la configuration peut ressembler à ceci :
brotli on;
brotli_comp_level 6;
brotli_static on;
brotli_types
text/plain
text/css
text/javascript
application/javascript
application/json
application/ld+json
application/xml
image/svg+xml;
L’option brotli_static on permet de servir des fichiers .br déjà générés si présents sur le disque. C’est très utile pour des applications construites avec Vite, Nuxt, Next.js en export statique ou tout autre pipeline de build.
Précompresser au moment du build
Pour les fichiers statiques, la précompression est souvent la meilleure stratégie. Elle déplace le coût CPU hors des requêtes utilisateurs.
Un script simple avec brotli et gzip peut suffire :
find dist -type f \( -name "*.js" -o -name "*.css" -o -name "*.html" -o -name "*.svg" -o -name "*.json" \) \
-exec gzip -k -9 {} \; \
-exec brotli -k -q 11 {} \;
Cette commande génère des variantes .gz et .br à côté des fichiers originaux. Le serveur peut ensuite choisir automatiquement la meilleure version selon le navigateur.
Pour un projet plus structuré, on peut intégrer cette étape au pipeline npm :
{
"scripts": {
"build": "vite build",
"postbuild": "node scripts/compress-assets.mjs"
}
}
Un script Node.js minimal peut ensuite parcourir le dossier de sortie :
import { createReadStream, createWriteStream } from "node:fs";
import { readdir, stat } from "node:fs/promises";
import { join } from "node:path";
import { createBrotliCompress, createGzip, constants } from "node:zlib";
const extensions = new Set([".html", ".css", ".js", ".json", ".svg"]);
async function walk(directory: string): Promise<string[]> {
const entries = await readdir(directory);
const files: string[] = [];
for (const entry of entries) {
const path = join(directory, entry);
const info = await stat(path);
if (info.isDirectory()) {
files.push(...await walk(path));
} else if ([...extensions].some((ext) => path.endsWith(ext))) {
files.push(path);
}
}
return files;
}
async function compressFile(path: string) {
await Promise.all([
new Promise<void>((resolve, reject) => {
createReadStream(path)
.pipe(createGzip({ level: 9 }))
.pipe(createWriteStream(`${path}.gz`))
.on("finish", resolve)
.on("error", reject);
}),
new Promise<void>((resolve, reject) => {
createReadStream(path)
.pipe(createBrotliCompress({
params: {
[constants.BROTLI_PARAM_QUALITY]: 11
}
}))
.pipe(createWriteStream(`${path}.br`))
.on("finish", resolve)
.on("error", reject);
})
]);
}
const files = await walk("dist");
await Promise.all(files.map(compressFile));
Pour un très gros projet, on limitera le parallélisme pour éviter de saturer la machine de build.
Compression dynamique dans Node.js
Dans une API Node.js, la compression à la volée reste utile pour les réponses JSON dynamiques. Avec Express, le middleware compression simplifie la configuration :
import express from "express";
import compression from "compression";
const app = express();
app.use(compression({
threshold: 1024,
filter: (request, response) => {
const type = response.getHeader("Content-Type");
if (typeof type === "string" && type.includes("text/event-stream")) {
return false;
}
return compression.filter(request, response);
}
}));
app.get("/api/products", async (request, response) => {
response.json({ products: [] });
});
app.listen(3000);
On évite ici de compresser les Server-Sent Events, car le buffering induit par la compression peut retarder la diffusion des messages. Pour ce cas, mieux vaut relire les contraintes abordées dans Server-Sent Events : diffuser des données en temps réel sans WebSocket.
Mesurer au lieu de supposer
Une bonne configuration doit être vérifiée. Dans les DevTools du navigateur, l’onglet Network permet de comparer la taille transférée et la taille réelle de la ressource. On peut aussi utiliser curl :
curl -H "Accept-Encoding: br" -I https://example.com/assets/app.js
La réponse devrait contenir :
Content-Encoding: br
Vary: Accept-Encoding
Pour gzip :
curl -H "Accept-Encoding: gzip" -I https://example.com/assets/app.js
La mesure ne doit pas s’arrêter à la taille transférée. Il faut aussi surveiller le temps serveur, le CPU et les métriques utilisateur réelles. L’approche décrite dans PerformanceObserver : mesurer les lenteurs réelles de vos interfaces web complète bien ce travail.
Compression, cache et invalidation
La compression fonctionne particulièrement bien avec des fichiers versionnés :
/assets/app.8f3a91c2.js
/assets/styles.91ab72d0.css
Ces noms contenant un hash permettent d’envoyer des en-têtes de cache longs :
Cache-Control: public, max-age=31536000, immutable
Quand le contenu change, le nom de fichier change aussi. Le navigateur peut donc garder l’ancienne version sans risque de servir un fichier périmé.
Cette logique rejoint les problématiques de cache applicatif abordées dans Cache frontend : rendre vos interfaces plus rapides sans données périmées, mais à un niveau différent : ici, on parle du cache HTTP des ressources statiques.
Pour le HTML, il faut être plus prudent. Une page HTML référence souvent les derniers fichiers générés par le build. On évitera donc un cache trop agressif sur le document principal :
Cache-Control: no-cache
no-cache ne veut pas dire “ne jamais stocker”. Cela signifie que le navigateur peut conserver la réponse, mais doit la revalider avant de la réutiliser.
Les erreurs fréquentes
La première erreur est de compresser deux fois. Cela peut arriver avec une combinaison mal maîtrisée entre CDN, reverse proxy et serveur applicatif. Le résultat est parfois une réponse illisible ou inutilement coûteuse.
La deuxième erreur est d’oublier Vary: Accept-Encoding, ce qui peut créer des comportements incohérents avec certains caches.
La troisième erreur est de compresser des flux qui doivent être transmis progressivement : SSE, streaming HTML, gros téléchargements ou réponses déjà fragmentées. La compression peut ajouter du buffering et retarder l’affichage.
La quatrième erreur est de se concentrer uniquement sur le ratio. Une compression Brotli niveau 11 peut être excellente au build, mais trop coûteuse à la volée. Pour les réponses dynamiques, un niveau modéré est souvent préférable.
Une stratégie simple et robuste
Pour une application web moderne, une bonne base consiste à :
- précompresser les assets statiques en Brotli et gzip au build ;
- servir Brotli en priorité, gzip en fallback ;
- compresser dynamiquement seulement les réponses textuelles suffisamment grandes ;
- exclure les formats déjà compressés et les flux temps réel ;
- vérifier systématiquement
Content-Encoding,VaryetCache-Control; - mesurer les effets en conditions réelles.
La compression HTTP n’est pas spectaculaire côté code, mais elle fait partie des optimisations à fort rendement. Elle améliore les performances sans imposer de changement d’architecture lourd, à condition d’être configurée avec précision.