12k
All articles

Gzip vs Brotli vs Zstd pour la compression des ressources web

Brotli, gzip ou zstd pour les ressources web: quand les utiliser, l effet des niveaux de compression et la précompression des fichiers statiques.

OpenReplay Team
OpenReplay Team
Gzip vs Brotli vs Zstd pour la compression des ressources web

Utilisez Brotli pour les ressources textuelles statiques, gzip comme solution de repli universelle, et zstd pour les réponses dynamiques lorsque vous maîtrisez à la fois le serveur et le CDN.

Beaucoup de bundles sont encore livrés en gzip parce qu’un outil de build ou une configuration d’hébergement par défaut en a décidé ainsi il y a des années, sans que personne ne se soit repenché depuis sur l’algorithme ou le niveau de compression. La suite de cet article explique pourquoi le contenu statique et le contenu dynamique constituent deux problèmes distincts, pourquoi le niveau de compression modifie souvent le résultat davantage que le nom de l’algorithme, comment précompresser au moment du build, comment le navigateur et le serveur négocient l’encodage, et ce qu’il faut laisser non compressé.

Points clés à retenir

  • Brotli à son niveau maximal est le bon choix pour le JavaScript et le CSS statiques, car le coût d’une compression lente se paie une seule fois au moment du build, et non à chaque requête.
  • Zstd convient aux réponses dynamiques : Cloudflare a mesuré une compression 42 % plus rapide que Brotli, avec une taille proche de celui-ci, pour des moyennes de 2,56:1 pour gzip, 2,86:1 pour zstd et 3,08:1 pour Brotli.
  • Le réglage du niveau compte autant que le nom de l’algorithme : passer à Brotli en conservant un niveau rapide par défaut revient à renoncer à une partie du gain.
  • La directive gzip_comp_level de nginx vaut 1 par défaut et brotli_comp_level de ngx_brotli vaut 6 ; aucune de ces valeurs par défaut n’est le maximum.
  • La négociation de l’encodage se joue entièrement dans les en-têtes Accept-Encoding et Content-Encoding : changer d’algorithme relève donc d’un paramètre serveur ou CDN, pas d’une modification du code applicatif.

Brotli vs Gzip vs Zstd : la recommandation

Brotli est le choix par défaut pour le JavaScript et le CSS statiques, car la compression s’effectue une seule fois au moment du build : les niveaux les plus élevés, pourtant lents, ne coûtent donc rien au moment de la requête.

Gzip reste dans la pile en tant que solution de repli, car tous les navigateurs le déclarent dans Accept-Encoding ; ce n’est le meilleur choix pour rien, mais c’est le seul qui ne fasse jamais défaut.

Zstd trouve sa place sur les réponses dynamiques, où le coût de compression est payé à chaque requête, car il atteint un ratio proche de celui de Brotli pour une fraction du temps CPU.

Statique vs dynamique : quand le coût de compression est-il payé ?

Le choix entre les algorithmes est en réalité un choix sur le moment où le coût CPU est payé. Un bundle statique est compressé une fois par release et servi des milliers de fois : seul le ratio compte, la vitesse de compression est sans importance. Une page HTML rendue à chaque requête est compressée à chaque appel : la vitesse de compression devient alors un enjeu de latence et de coût serveur au même titre que le ratio.

L’annonce de la prise en charge de Zstandard par Cloudflare en septembre 2024 fournit les chiffres pour le cas dynamique. Lors d’un test mené au troisième trimestre 2024, qui a basculé pendant 24 heures le trafic du plan Free de Brotli vers zstd — zstd réglé sur son niveau 3 par défaut, les niveaux de Brotli et gzip n’étant pas précisés — zstd a compressé 42 % plus vite que Brotli tout en s’en approchant en termes de taille. Les moyennes mesurées étaient de 2,56:1 pour gzip, 2,86:1 pour zstd et 3,08:1 pour Brotli, et la conclusion de Cloudflare elle-même est que zstd est mieux adapté aux réponses dynamiques, HTML compris.

Verdict : Brotli l’emporte lorsque le ratio est le seul critère, ce qui décrit toute ressource statique. Zstd l’emporte lorsque la compression s’exécute à chaque requête.

Le niveau de compression compte autant que l’algorithme

Chaque algorithme expose un réglage de niveau, et ce niveau influe souvent davantage sur le résultat qu’un changement d’algorithme. Brotli propose les niveaux 0 à 11 dans ngx_brotli, gzip les niveaux 1 à 9 dans nginx, et l’interface en ligne de commande zstd les niveaux 1 à 19 avec une valeur par défaut de 3, auxquels s’ajoutent les niveaux 20 à 22 derrière --ultra.

L’ampleur de l’effet du réglage dépend entièrement des données d’entrée : les chiffres avant/après publiés pour le bundle de quelqu’un d’autre ne vous apprennent donc presque rien sur le vôtre. Passez vos fichiers dans l’outil de comparaison de compression, qui compresse en gzip, Brotli et zstd à n’importe quel niveau directement dans le navigateur, et comparez les tailles par vous-même.

Les valeurs par défaut expliquent pourquoi tant de sites sont livrés avec des réglages rapides. La directive gzip_comp_level de nginx vaut 1 par défaut, le plus rapide des niveaux 1 à 9. brotli_comp_level de ngx_brotli vaut 6 sur une échelle de 0 à 11. Aucun n’est le maximum, et les deux sont pertinents pour une compression à la volée — soit exactement le mauvais contexte pour des ressources statiques. Quel que soit le niveau retenu, il se paie du côté de la compression. Le README du projet zstd souligne lui-même que le décodage s’exécute à peu près à la même vitesse quel que soit le niveau ayant produit le fichier, ce qui vaut également pour zlib et lzma.

Verdict : réglez le niveau de manière délibérée. Le maximum pour tout ce qui est précompressé ; un niveau intermédiaire pour tout ce qui est compressé à chaque requête.

Comment précompresser les ressources au moment du build ?

La précompression consiste à faire écrire par l’étape de build un fichier voisin .br, .gz et éventuellement .zst à côté de chaque ressource ; le serveur sélectionne alors le fichier correspondant sans rien compresser au moment de la requête.

brotli -q 11 app.js    # writes app.js.br; source kept by default
gzip -9 -k app.js      # writes app.js.gz; -k keeps the source
zstd -19 app.js        # writes app.js.zst; source kept by default

L’interface en ligne de commande brotli conserve les fichiers d’entrée par défaut et accepte -q pour une qualité de 0 à 11. GNU gzip requiert -k ou --keep pour laisser l’original en place.

Servir ces fichiers voisins dans nginx tient en deux directives :

load_module modules/ngx_http_brotli_static_module.so;

http {
    gzip_static   on;   # serves .gz siblings; module needs --with-http_gzip_static_module
    brotli_static on;   # serves .br siblings; default off
    gzip_vary     on;   # adds Vary: Accept-Encoding; default off
}

ngx_http_gzip_static_module n’est pas compilé par défaut. brotli_static provient de ngx_brotli. nginx ne fournit aucun module zstd natif ; le module tiers zstd-nginx-module ajoute une directive zstd_static pour les fichiers voisins .zst, désactivée par défaut. Sans lui, zstd pour les fichiers statiques relève d’un paramètre côté CDN.

Les CDN laissent passer les fichiers précompressés. La documentation de Cloudflare sur la compression indique qu’il conserve l’en-tête content-encoding: br ou gzip de l’origine lorsque le navigateur du visiteur le prend en charge et qu’aucune fonctionnalité de réécriture de réponse (Rocket Loader, Email Address Obfuscation, Polish, etc.) n’est activée ; il interroge les origines avec accept-encoding: br, gzip, de sorte que le zstd d’origine n’est pas transmis. Le comportement Brotli Support d’Akamai sert et met en cache le Brotli compressé à l’origine, renvoie des variantes non Brotli aux clients qui n’acceptent pas br, et n’effectue lui-même aucune compression en périphérie.

Verdict : compressez au moment du build, au niveau maximal, et configurez le serveur ou le CDN pour servir le fichier voisin.

Comment le navigateur et le serveur s’accordent-ils sur un encodage ?

Le navigateur énumère les encodages qu’il sait décoder dans Accept-Encoding, le serveur ou le CDN en choisit un et étiquette la réponse avec Content-Encoding ; aucun code applicatif n’intervient dans cet échange.

GET /app.3f2a1b.js HTTP/1.1
Accept-Encoding: gzip, deflate, br, zstd
HTTP/1.1 200 OK
Content-Encoding: br
Vary: Accept-Encoding

br est le jeton de Brotli dans le registre des content-codings HTTP, défini dans la section 13 de la RFC 7932. Vary: Accept-Encoding indique aux caches partagés de ne pas transmettre un corps Brotli à un client qui n’a demandé que du gzip ; nginx omet cet en-tête sauf si gzip_vary on est défini.

Pour vérifier ce qu’un site sert aujourd’hui, demandez un fichier de bundle et lisez le seul en-tête qui vous intéresse dans la réponse :

curl -sI -H 'Accept-Encoding: gzip, br, zstd' https://example.com/app.js | grep -i content-encoding

La même information apparaît dans le panneau Network des DevTools, sous les en-têtes de réponse.

La prise en charge par les navigateurs détermine l’ordre de repli. Brotli est pris en charge par tous les navigateurs majeurs actuels. caniuse recense zstd dans Chrome et Edge à partir de la version 123, Firefox à partir de 126, Opera à partir de 109 et Safari à partir de 26, la version desktop de Safari étant marquée comme prise en charge partielle. Tout navigateur qui omet zstd de Accept-Encoding reçoit simplement du Brotli ou du gzip. Il n’y a pas de mode d’échec, seulement un repli.

Verdict : changer d’algorithme est une modification de configuration, et la chaîne de repli rend l’opération sans risque.

Que ne faut-il pas compresser ?

Recompresser du JPEG, du MP4 ou du WOFF2 gaspille du CPU des deux côtés pour un gain quasi nul, car ces formats sont déjà compressés en interne. WOFF2 est le cas le plus flagrant : la section 1.2 de la RFC 7932 précise que le format qu’elle définit est intégré à WOFF 2.0 ; un fichier .woff2 est donc déjà une sortie Brotli.

Les réponses de très petite taille constituent l’autre exclusion. En dessous de quelques dizaines d’octets, la surcharge d’encodage dépasse le gain : c’est pourquoi gzip_min_length de nginx et brotli_min_length de ngx_brotli valent 20 octets par défaut, et que Cloudflare ne compresse que les réponses d’au moins 48 octets pour gzip et 50 octets pour Brotli et zstd.

Les conclusions présentées ici s’appliquent aux fichiers que les navigateurs reçoivent réellement, de quelques kilo-octets à quelques méga-octets. Les benchmarks montrant Brotli mettre plusieurs minutes portent sur des fichiers de plusieurs centaines de méga-octets qui ne transitent jamais via Content-Encoding.

Verdict : compressez le texte (HTML, CSS, JavaScript, JSON, SVG) ; écartez les médias, les polices et les réponses minuscules.

Conclusion

La question de l’algorithme a une réponse établie : Brotli au niveau maximal pour tout ce qui est compressé au moment du build, zstd pour tout ce qui est compressé à chaque requête, gzip comme socle que tous les clients comprennent. La question du niveau, elle, est celle que la plupart des piles techniques ne se sont jamais posée. Lancez la vérification curl sur votre propre bundle, notez l’encodage et déduisez le niveau de la taille obtenue ; si la réponse est « gzip à un niveau rapide par défaut », la précompression en Brotli ne se trouve qu’à une étape de build et deux directives serveur de distance.

FAQ

Pourquoi mon site envoie-t-il encore du gzip alors que le navigateur prend en charge Brotli ?

Trois causes expliquent la quasi-totalité des cas. Premièrement, Chrome et Firefox n'annoncent « br » dans Accept-Encoding qu'en HTTPS : les requêtes en HTTP simple, y compris la plupart des développements en localhost, retombent donc sur gzip. Deuxièmement, aucun module Brotli n'est chargé sur le serveur ; nginx a besoin de ngx_brotli, qui n'est pas intégré d'origine. Troisièmement, un CDN comme Cloudflare laisse passer une réponse d'origine déjà étiquetée content-encoding: gzip au lieu de la transcoder en Brotli.

Quelle est la différence entre « deflate » et « gzip » dans Content-Encoding ?

Les deux reposent sur le même algorithme DEFLATE issu de la RFC 1951 et ne diffèrent que par leur enveloppe. En HTTP, « deflate » désigne le format zlib de la RFC 1950 (en-tête de deux octets, somme de contrôle Adler-32) et « gzip » le conteneur de la RFC 1952 avec un pied de fichier CRC-32. Les premiers serveurs et navigateurs envoyaient parfois du DEFLATE brut sous le nom « deflate », obligeant les clients à deviner : gzip est ainsi devenu le choix fiable, et deflate vaut rarement la peine d'être proposé.

Node.js prend-il en charge nativement la compression Brotli et zstd ?

Oui. Le module intégré node:zlib implémente les content-encodings gzip, deflate, br et zstd sans package tiers. Brotli est disponible depuis Node.js 11.7.0 via zlib.brotliCompress et zlib.createBrotliCompress ; Zstandard est arrivé avec Node.js 23.8.0 via zlib.zstdCompress et zlib.createZstdCompress, et Node.js 24.6.0 a ajouté la prise en charge des dictionnaires aux API zstd (https://nodejs.org/en/blog/release/v24.6.0). Les middlewares de compression antérieurs à ces versions peuvent ne négocier que gzip : vérifiez le content-encoding qu'ils émettent.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.