Des tuyaux jamais assez gros
C'est bien de s'extasier sur sa nouvelle connexion fibre à très haut débit et d'utiliser les benchmarks Speedtest pour savoir qui pisse le plus loin.
Mais un monde (ou un pays, selon ses ambitions) à très haut débit n'est pas réaliste, ni même souhaitable. Le ratio énergie / bande passante n'est pas linéaire, faire croître la bande passante de façon uniforme dans le monde entier représente un coût énergétique faramineux ; à l'heure du régime bas carbone cette proposition est absurde.
La qualité des connexions est variable, que ça passe par un réseau filaire (cuivre ou optique), ou hertzien, les connectés sont loin d'être égaux.
La première approche pour régler ça est de limiter l'obésité des pages webs. Donc mettre moins d'images, moins de vidéos, et choisir des typographies moins lourdes. Cette solution marche mais elle repose sur un travail en amont d'équipes éditoriales et de développeurs, ce qui peut être onéreux, et peut donner le sentiment de brimer leur créativité.
La seconde approche est de déployer du très haut débit partout, avec des techniques fun, à base de ballons captifs stratosphériques , de WiMax ou de bêtes satellites . Si on est patriote, on peut aussi regretter les efforts inaboutis du Bi-Bop . Cette solution, comme dit plus haut n'est pas réaliste pour le moment.
La troisième approche, complémentaire, est de confier le problème aux matheux et de compresser tout ce que l'on peut. C'est de cette troisième solution dont nous souhaitons vous parler aujourd'hui.
État de l'art de la compression web
Une partie des contenus du web ont des formats intégrant de la compression destructive ou non destructive. Les plus vieux se souviendront, émus de l'arrivée du MP3 (aka MPEG 1 Layer 3) prérequis à la Hadopi que le monde nous envie.
Les médias, images fixes ou mouvantes et le son, sont compressés depuis longtemps. Une image vaut mille mots, c'est pour ça que c'est mille fois plus gros.
Compression du tuyau, pas du format
HTTP a dans sa norme la notion de compression, l'entête Content-Encoding
que l'on négocie avec Accept-Encoding
. En précisant un Content-Encoding
, on peut compresser tout ce qui ne l'est pas déjà, du texte généralement (HTML, CSS, JS, XML…).
Des petits malins ont eu l'idée de proposer de la compression sur la couche TLS (SSL à l'époque), ça a fini avec deux failles de sécurité avec leur CVE (Common Vulnerabilities and Exposures) et leur acronyme prestigieux : CRIME et BREACH ).
Il ne faut pas mixer compression et chiffrement. Par contre, confier un contenu compressé à une connexion chiffrée fonctionne très bien, rassurez-vous.
gzip traîne sur Internet (et UNIX) depuis longtemps, avec un ratio compression/temps de calcul qui reste tout à fait honorable.
C'est donc en toute logique que gzip est l'outil de compression présent dans HTTP depuis toujours ou presque. DEFLATE est aussi largement répandu, mais avec des performances similaires à gzip, donc tout le monde s'en fout.
Le renouveau de la compression
Avec l'invasion des connexions nomades (et compulsives), Google s'est inquiété des latences et des débits si pénalisants à l'expérience utilisateur.
Google a donc commencé par lâcher un pâtissier suisse sur la question de l'optimisation. À moins que ce soit une équipe de chercheurs adeptes des viennoiseries suisses.
Le marché aujourd'hui
Zopfli
Zopfli est un logiciel de compression qui reprends l'algorithme de gzip, aka zlib, pour faire mieux, en prenant plus de temps. C'est donc prévu pour de la compression à froid, surtout pas à la volée. Ce gain en efficacité peut profiter au PNG, ou à n'importe qui réclamant Accept-Encoding: gzip
.
Le gain en compression est de 3 à 8 %, pour un temps de compression 80 fois supérieur, et un temps de décompression similaire.
Brotli
Pour aller plus loin dans l'amélioration de la compression, il faut donc de nouveaux algorithmes, et donc perturber les petites habitudes des navigateurs. HTTP propose depuis toujours la négociation d'encoding, et ce n'est pas délirant de proposer une nouveauté avec une solution de repli pour les conservateurs (gzip) et les flemmasses (pas d'encoding).
Niveau lobbying, Google est un champion hors catégorie, et du coup Chrome le géra très vite (en 2016). Mozilla met un point d'honneur à suivre très vite ce genre d'innovations, tant qu'il n'y a pas de pièges avec des brevets. Donc, Firefox le gère, pour peu que l'on soit sur une connexion sécurisée. OK, il y a un bug qui traîne depuis quelques années, Firefox boude brotli en localhost
.
On peut maintenant affirmer que Brotli est largement supporté par les navigateurs .
Brotli est très peu exigeant en RAM pour la décompression, ciblant les téléphones manquant d'ambition. Techniquement, il utilise un dictionnaire énumérant les éléments communs à tout ce qu'il compresse, 13 000 éléments (mots, phrases) tenant dans 120 Ko. Google a peaufiné ce dictionnaire avec tout le HTML qu'il aspire d'Internet et qu'il estime être commun à tous les utilisateurs.
Le gain annoncé face à gzip est de 20 % pour du HTML, 15 % pour le JavaScript et les feuilles de style.
Zstandard
Quand il faut jeter des chercheurs sur un problème, Facebook n'est jamais loin. Avec des besoins gargantuesques de bande passante similaire à ceux de Google, ils ont d'abord ciblé leurs besoins internes, en privilégiant les temps de compression/décompression, Google ayant son snappy ) pour ça.
zstd utilise le même concept de dictionnaire que Brotli, mais permet de le générer, pour l'adapter à son propre corpus.
Zstd est maintenant normalisé (IANA + une RFC) et son utilisation comme Content-encoding
est prévu, mais bon, grosse flemme depuis 2 ans, et comme Facebook ne détient pas l'un des deux moteurs de rendus HTML du marché, bah il n'a pas la motivation pour faire le lobbying nécessaire à son adoption.
Compression de serveurs
Gzip est géré par tous les serveurs webs de la Terre, et même les serveurs applicatifs proposent des middlewares pour ça. Il n'y a pas d'excuses pour ne pas l'activer.
Brotli côté serveur
Pour avoir du Brotli côté serveur, c'est clairement moins la fête qu'avec gzip.
Google propose des binding de référence pour brotli sans que ça provoque un engouement.
Le même Google fournit un module nginx , marqué comme "en développement actif", et qui n'est pas packagé Debian.
Node a du mal à finaliser son middleware expressjs .
En Python, Django a un middleware , et WhiteNoise le gère tranquillement.
PHP a son module capable de compresser le flot de sortie à la demande , mais qui déploie des modules PHP compilés à la mimine ?
Rails a la flemme et délègue tout à Nginx.
Go est coincé par la lib brotli qui utilise cgo
et aucun proxy web ne le propose en standard. Caddy se contente de gérer les assets compressés à froid.
Ne vous inquiétez pas pour Akamai ou Cloudflare , ils gèrent très bien brotli, et en dynamique. Pour Cloudflare, c'était inévitable, ce sont des gros utilisateurs de Nginx, et ils n'hésitent pas à lui écrire des modules (et à inventer des CVE quand la mémoire est mal gérée), donc bon, compiler le bout de code de Google ne doit pas leur faire peur.
Servir des assets précompressés
Techniquement, les assets sont rarement dynamiques, webpack fait suffisamment de magie pour tout préparer. Les seuls contenus mobiles seront du HTML, du JSON et du XML pour les traditionalistes. Quand on fait des sites statiques (gatsby, hugo, lektor…), c'est le jackpot, tout le contenu peut être préparé.
Avec une seule convention de nom : si à côté du fichier toto.css
se trouve le fichier toto.css.gz
le serveur web se contentera d'envoyer le contenu de toto.css.gz
quand un client réclame toto.css
avec Accept-Encoding: gzip
.
Il faut un pipeline correct, pour garantir que jamais la version compressée ne sera désynchronisée.
Historiquement, Rails proposait de le faire à la volée, une idée catastrophique : ce genre de pipeline peut facilement avaler quelques gigas de RAM et faire swapper la machine à mort, quand ce n'est pas un OOM direct. Dans tous les cas, ça explose la qualité du service, et quand on a plusieurs frontaux, on recommence sur chacun, en multipliant les dégâts. Il ne faut jamais faire ce genre de chose, et le désactiver systématiquement. Ensuite, les mêmes malins ont eu l'idée de pousser les assets dans git, pour continuer à déployer avec git. Pousser des données binaires, qui vont changer régulièrement, dans un git, est à la limite du sacrilège. Déployer avec un conteneur ou un rsync suffit largement.
Servir des assets précompilés avec Lua
Quand on n'a pas envie de suivre les évolutions de ngx_brotli et de recompiler régulièrement son nginx, on peut tout simplement utiliser un peu de Lua.
Lua est le langage de prédilection pour embarquer des règles métiers dans du code en C. Les joueurs de World Of Warcraft sont là pour le confirmer.
L'intégration de Lua dans Nginx est propre, mais il est indispensable de connaître les différentes phases du traitement d'une requête nginx . Ces étapes sont impitoyables : si on agit trop tôt ou trop tard, ça ne marchera pas, sans lever d'erreurs.
server { set $index_br index.html; set $is_br off; set $original ""; default_type text/html; set_by_lua_block $uri_br { ngx.var.original = ngx.header.content_type local ae = ngx.req.get_headers()["Accept-Encoding"] if (ae ~= nil and string.find(ae, "br")) then ngx.var.index_br = "index.html.br" ngx.var.is_br = "on" return ngx.var.uri .. ".br" else return ngx.var.uri end } }
Si la requête a l'entête Accept-Encoding
et qu'il contient br
, on active la gestion du brotli. Oui, on s'assoit sur la priorité des encoding, et l'on prend le risque qu'il existe un autre encoding qui contienne les lettres b
et r
. On aura noté que les variables sont initialisées côté Nginx, avant d'être modifiées dans le Lua.
location / { # First attempt to serve request as file, then # as directory, then fall back to displaying a 404. index $index_br index.html; try_files $uri_br $uri $uri/ =404; if ($is_br = off) { gzip on; } rewrite_by_lua_block { if (ngx.var.is_br == "on") then ngx.header.content_encoding = "br" ngx.header.content_type = ngx.var.original end } }
Pour les dossiers, on cherchera des fichiers index.html.br
. On regardera ensuite si une uri correspond à un ficher suffixé par .br
. Nginx ne sait pas gérer des extensions de fichier composées, donc, pour gérer les index.html.br
on met text/html
comme type par défaut.
On n'active gzip que si on n'a pas de brotli. C'est pour être sur, mod_gzip vérifie que le Content-Encoding
est vide avant d'essayer de compresser.
On ajoute l'entête Content-Encoding: br
dans la réponse, et l'on utilise le type mime du fichier, avant que l'on réclame sa variante brotlisée.
Le fichier complet est disponible en ligne .
Pourquoi finasser sur la compression ?
Par ce que votre page web ne va ressembler à quelque chose qu'une fois les assets chargés, et que pour fluidifier l'expérience de navigation au sein du site, vous allez télécharger un maximum d'éléments dès la première page, pour ensuite piocher dedans lors des pages suivantes. Autant réduire ce temps d'attente.
Google a décidé que les gens devaient être heureux et pressés sur Internet, et donc qu'il fallait favoriser les sites qui font les efforts nécessaires pour que ça arrive. Ça s'appelle le SEO, et compresser correctement fait partie des éléments améliorant le SEO, et l'expérience utilisateur.
Être conscient de la consommation de bande passante (et en énergie finalement) que l'on impose à son public et en modérer l'impact est la moindre des politesses.