1 points par GN⁺ 2024-09-08 | 1 commentaires | Partager sur WhatsApp
  • Comme GitHub Pages ne prend pas en charge Brotli, il s’agit d’une expérimentation consistant à encoder du HTML en image WebP sans perte, puis à le restaurer via le décodeur d’images du navigateur et JavaScript afin de réduire le volume transféré
  • Les décodeurs Brotli en WASM ajoutent un surcoût de 71 KiB à 200 KB, et la Compression Streams API ne prend en charge que gzip, deflate et deflate-raw, ce qui en fait difficilement une voie de contournement pour le décodage Brotli
  • Le WebP sans perte VP8L peut produire, même sur des données textuelles, de meilleurs résultats que gzip grâce à sa transformation prédictive, à la réutilisation d’arbres Huffman par blocs de 16x16, et à son color cache
  • Un HTML de test de 439,478 octets a été réduit à 94,683 octets avec gzip et jusqu’à 43,182 octets avec WebP ; c’est plus gros que les 37 KiB de Brotli, mais environ 2,2 fois plus petit que gzip
  • À cause du bruit anti-fingerprinting de Canvas 2D, des changements sur le readPixels de WebGL, de l’écran vide initial et des problèmes de restauration du scroll, cela ressemble davantage à un hack révélant les contraintes des navigateurs qu’à une solution réellement applicable

Le problème : impossible d’utiliser Brotli sur GitHub Pages

  • Pour réduire le temps de chargement d’une page, la compression HTTP a plus d’effet qu’un simple minify du HTML
  • HTTP prend en charge gzip et Brotli via l’en-tête Content-Encoding
    • gzip est peu coûteux et souvent activé par défaut
    • Brotli compresse généralement mieux que gzip, mais il est nettement plus lent
  • GitHub Pages ne prend pas en charge Brotli ; l’article le plus long du site, Recovering garbled Bitcoin addresses, ferait 37 KiB en Brotli, mais monte à 92 KiB en gzip
  • Cet écart allonge inutilement le temps de chargement d’un facteur 2,5

Les premières alternatives envisagées, et leurs limites

  • Le problème serait évité si GitHub permettait d’uploader et de servir des fichiers Brotli précompressés, mais cette fonctionnalité n’existe pas
  • Décompresser côté client directement en JavaScript réduit fortement l’intérêt à cause de la taille du décodeur WASM
    • brotli-dec-wasm fait environ 200 KB
    • tiny-brotli-dec-wasm fait 71 KiB
    • La comparaison devient donc gzip 92 KiB contre contenu Brotli 37 KiB + 71 KiB, ce qui annule pratiquement le gain
  • La pile HTTP du navigateur contient bien un décodeur Brotli, mais DecompressionStream de la Compression Streams API n’accepte que gzip, deflate et deflate-raw
  • Même un gzip précompressé avec Zopfli reste à 86 KiB, donc plus gros que Brotli

Utiliser un format d’image comme conteneur de compression

  • Les navigateurs savent déjà décoder les images ; il suffit donc d’insérer les données dans des pixels, puis de les relire via l’API Canvas, sans avoir à ajouter une nouvelle logique de décompression
  • GIF déroule les données en ordre row-major puis applique LZW, mais DEFLATE de gzip a justement été conçu pour remplacer LZW, donc il est difficile d’en attendre un bénéfice
  • PNG utilise DEFLATE, mais applique d’abord une transformation prédictive qui compresse non pas les pixels bruts, mais leurs différences avec les pixels voisins
    • Exemple : au lieu de compresser [a, b, c, d], on compresse [a, b-a, c-b, d-c]
    • Plus l’écart entre valeur prédite et valeur réelle est faible, plus la compression Huffman est efficace
  • Le cœur de l’expérimentation consiste à détourner le WebP sans perte, VP8L, pour compresser des données binaires générales

Ce qui différencie VP8L de gzip

  • WebP existe en variantes avec perte et sans perte ; ici, seul le format sans perte VP8L est concerné
  • Comme PNG, VP8L utilise une transformation prédictive, mais remplace DEFLATE par un algorithme proche conçu par Google
  • DEFLATE découpe un fichier en plusieurs blocs, chacun pouvant avoir son propre arbre Huffman
    • Dans un HTML qui mêle JavaScript, SVG et balisage, différents arbres peuvent être avantageux
  • VP8L permet de définir une table d’arbres Huffman arbitrairement grande et d’utiliser un arbre différent pour chaque bloc de 16x16 pixels
    • Si du JavaScript est suivi de CSS puis à nouveau de JavaScript, DEFLATE peut devoir réencoder plusieurs fois des arbres similaires
    • VP8L permet de réutiliser ces arbres, et donc de les changer plus souvent, à moindre coût
  • Le color cache de VP8L permet aussi de représenter brièvement une valeur en disant, en substance, de recopier un pixel récent ayant certaines propriétés

Première expérimentation de compression en WebP

  • Le fichier de test est le HTML de Recovering garbled Bitcoin addresses
    • Taille d’origine : 439,478 octets
    • gzip --best : 94,683 octets
  • Les octets ont été transformés en image RGB en niveaux de gris puis compressés en WebP sans perte à l’aide de la crate Rust webp
  • Le niveau de gris a été choisi à cause de la transformation WebP subtract green
    • En niveaux de gris, si l’on soustrait le canal G aux canaux R/B, R/B deviennent pratiquement nuls
    • WebP encode les trois canaux avec des arbres Huffman séparés, donc les canaux constants occupent un espace proche de O(1)
  • L’idée initiale était de créer une image 1xN, mais WebP ne prend en charge que jusqu’à 16383x16383, ce qui a provoqué l’erreur VP8_ENC_ERROR_BAD_DIMENSION
  • En passant à une forme 16383xN, le résultat est descendu à 45,604 octets, soit deux fois plus petit que gzip et même plus petit que bzip2 à 49,764 octets

Ajustements spécifiques à WebP

  • Sur une image large en ordre row-major, des octets éloignés dans l’entrée se retrouvent mélangés au sein d’un même bloc 16x16
  • En changeant la forme de l’image pour une version verticale 27x16383, le résultat est tombé à 43,232 octets
  • Le paramètre de compression method de cwebp a été comparé de 0 à 6
    • method 0 : 48,902
    • method 1 : 43,546
    • method 2 : 43,442
    • method 3 : 43,292
    • method 4 : 43,232
    • method 5 : 43,182
    • method 6 : 43,182
  • La méthode 5 a été retenue, car elle produit la même taille que la méthode 6 tout en étant plus rapide
  • À ce stade, WebP est 2,2 fois plus petit que gzip, mais 1,2 fois plus gros que Brotli

Benchmarks sur plusieurs fichiers

  • Les jeux de comparaison comprenaient les snappy testdata, le Canterbury Corpus et Large Corpus, ainsi que deux fichiers SVG
  • Les formats comparés étaient gzip --best, brotli --best, bzip2 --best et le script de compression WebP
  • À part quelques très petits fichiers comme grammar.lsp, xargs.1 et quelques exceptions, WebP faisait presque toujours mieux que gzip
  • Les exceptions étaient kennedy.xls et paper-100k.pdf
    • paper-100k.pdf contient 19 KB de XML suivis de données déjà compressées ; la mesure revient donc essentiellement à tester un petit volume de données
    • kennedy.xls présente aussi un comportement inhabituel vis-à-vis de Brotli et bzip2 ; il peut s’agir d’un fichier difficile à compresser parce qu’il mélange, à courte distance, des données très hétérogènes
  • WebP est généralement un peu moins bon que bzip2, mais le dépasse dans certains cas
  • WebP est toujours moins bon que Brotli, sauf sur quelques cas extrêmes comme fireworks.jpeg, proche d’un blob aléatoire presque uniforme
  • Sur de gros jeux de données en texte brut, l’amélioration par rapport à gzip reste mesurable
    • Il y avait aussi un gain sur les fichiers SVG
    • Sur html_x_4, WebP atteignait 3.3%, moins bon que les 2.8% de Brotli, mais bien meilleur que les 13% de gzip

Restaurer le contenu avec JavaScript

  • Le décodage WebP lui-même peut être mis en œuvre avec fetch, createImageBitmap, OffscreenCanvas, getImageData et TextDecoder
  • Le principe consiste à prendre le canal R des pixels comme octets HTML d’origine, à les décoder en UTF-8, puis à les injecter dans document.documentElement.innerHTML
  • L’API Canvas est souvent utilisée pour le fingerprinting, donc certains navigateurs ajoutent du bruit au résultat de getImageData
    • Avec la protection stricte contre le pistage de Firefox, moins de 1 % des pixels peuvent être touchés
    • Dans du HTML, ce bruit apparaît comme des fautes de frappe
  • En utilisant readPixels de WebGL, cela fonctionnait alors sans bruit
    • WebGL ne garantit toutefois la prise en charge stable des textures que jusqu’à 2048x2048, ce qui impose à nouveau des contraintes de taille
    • Une fois minifié, ce code de restauration faisait environ 550 octets
  • WebP et le code réunis totalisaient 44 KiB, à comparer aux 92 KiB de gzip et aux 37 KiB de Brotli
  • Après le 15 avril 2026, Firefox a ajouté l’anti-fingerprinting à readPixels aussi, de sorte que cette méthode ne fonctionne plus telle quelle

Clignotement de l’écran et problème de scroll

  • await repose sur des promesses ; avant la fin du téléchargement du WebP, le navigateur considère donc que l’exécution du script est terminée
  • Comme le DOM est encore vide, l’utilisateur voit brièvement un écran blanc vide
  • Pour atténuer cela, on peut conserver dans le HTML gzip les styles et environ 8 KiB du haut de page, et ne compresser en WebP que le contenu situé sous le viewport
  • La restauration de la position de scroll au rechargement pose aussi problème
    • Par exemple, si l’on recharge à la position Y = 5000px alors que la page mesure 0px de hauteur, cette position est réinitialisée
    • Ajouter un très grand div temporaire peut aider
  • Il faut affecter document.documentElement.innerHTML plutôt que d’utiliser document.write, afin de mettre à jour le document courant sans le remplacer par un nouveau

Intégrer directement le WebP dans le JavaScript

  • Pour réduire encore un peu la latence, le WebP peut être embarqué directement dans le JavaScript
  • La méthode la plus simple est une data URL en base64
  • La base64 augmente la taille d’origine d’un facteur 1.33, mais gzip compense presque entièrement cette hausse
    • Converti en base64, compressed.webp fait 57,576 octets
    • Compressé ensuite avec gzip --best, cela donne 43,519 octets
  • Un blob compressé comme un WebP ressemble presque à des données aléatoires uniformes, et la transformation 8 bits vers 6 bits de la base64 permet à l’arbre Huffman de gzip d’agir pratiquement comme une transformation inverse
  • On pourrait aussi utiliser Unicode et UTF-16, mais la base64 reste une première solution suffisante

Mise en pratique et état actuel

  • Au moment de la rédaction, cette page elle-même était compressée en WebP à partir de la section « Fool me twice », sauf sur les anciens navigateurs ou lorsque JavaScript était désactivé
  • Dans le code réel, l’image WebP de la page était longue et étroite, mais un exemple plus visuel sous forme de WebP carré était aussi fourni
  • Dans cette image, les zones claires en haut et en bas correspondaient au texte et au code, la zone hachurée vers un cinquième de la hauteur à un diagramme, et la plupart des zones sombres au texte présent dans ce diagramme
  • Le gain réel restait limité
    • Page gzip d’origine : 88 KiB
    • Page gzip après application de WebP : 83 KiB
    • Estimation avec Brotli : 69 KiB
  • Après le 15 avril 2026, la page est revenue à une version non compressée afin d’éviter que les visiteurs sur Firefox ne voient du contenu corrompu
  • Le code Rust, le corpus et les autres fichiers sont publiés sur GitHub

1 commentaires

 
GN⁺ 2024-09-08
Avis de Hacker News
  • Si l’on ignore la latence, sans doute, mais en pratique le temps de chargement semble n’augmenter que d’environ 0,001 %
    Le surcroît de taille est négligeable par rapport à la latence aller-retour, et le temps passé à décompresser pourrait même être supérieur au temps économisé en transférant 55 Kio de moins
    C’est une expérience intéressante, mais dans ce cas l’expérience utilisateur risque plutôt de se dégrader, avec une vitesse presque identique et seulement une compatibilité moindre

    • C’est vrai si l’on n’optimise que le temps de chargement et que l’on suppose les débits de tout le monde, mais j’aimerais souvent que les auteurs de sites web ou d’apps ne décident pas aussi facilement à ma place du compromis vitesse/données
      Le problème, c’est qu’il y a des gens comme l’auteur de l’article qui s’efforcent de faire passer 100 Ko à 50 Ko, mais aussi des sites qui m’envoient sans sourciller des dizaines de Mo d’images alors que je veux juste consulter les horaires d’ouverture d’un restaurant en roaming
      La conscience des ressources existe, mais elle est malheureusement très inégalement répartie
    • Ce n’est pas seulement une question de temps de décompression. Il faut télécharger l’ensemble avant de pouvoir le décompresser, alors qu’un navigateur peut décompresser et rendre immédiatement le HTML reçu en streaming depuis le serveur
      Si la connexion se coupe, on perd tout, et il devient même impossible de lire ne serait-ce que la partie téléchargée
      Sur une connexion normale, la différence n’a pas d’importance ; sur une connexion très lente ou instable, au point que 50 Ko comptent, cette méthode est clairement pire. Expérience amusante, mais j’aimerais qu’on ne l’applique pas à un site
    • S’il n’est pas en cache, il y a un fichier Symbols-2048-em%20Nerd%20Font%20Complete.woff2 de 850 Kio, ce qui efface presque la différence
    • Un écart de taille de cet ordre est assez important pour influencer le nombre d’allers-retours nécessaires. Avec une valeur moderne raisonnable de fenêtre de congestion initiale, on devrait économiser à peu près un aller-retour
      Ce ne sera pas un facteur 2,5, mais ce n’est pas 0,001 % non plus
    • Si l’économie est inférieure à une fenêtre de réception TCP, cela ne changera rien à la latence
      Sur un réseau avec pertes, cela pourrait faire une différence, mais je n’en suis pas certain
  • Je ne comprends pas pourquoi readPixels n’est pas visé par les protections anti-empreinte. Ça me va, puisqu’on ne dissémine pas sur toute la page des fautes de frappe presque invisibles
    Le passage expliquant que le HTML gzipé ne contient que les styles et environ 8 Kio du haut de page, et que seul le contenu sous le viewport est compressé en WebP, explique pourquoi l’article se coupait soudainement après une phrase quelconque avant d’afficher une page blanche
    J’utilise LibreWolf, donc WebGL est désactivé, et j’utilise Chromium pour les jeux web aléatoires qui nécessitent WebGL. En activant WebGL, l’article fonctionnait bien et, franchement, c’est une technique assez élégante

    • Si cela ne fonctionne pas dans tous les navigateurs web modernes, même avec la protection contre l’empreinte activée, et qu’il n’y a pas de fallback pour les anciens navigateurs, c’est difficile d’appeler ça élégant
      Le WWW devrait rester universellement accessible en partant de HTML ordinaire et en s’améliorant progressivement
  • Il est certes possible d’utiliser Brotli directement dans un navigateur web, mais avec des contraintes évidentes
    Je pense que la soumission JS1024 de 2022 [1] a été la première démonstration de ce concept, et il existe aussi un code de preuve de concept pour de la compression arbitraire. Malheureusement, cela ne convenait pas au codage de taille qui était l’objectif initial
    La contrainte principale est qu’on est de fait limité aux caractères ASCII et, pour des raisons évidentes, que c’est très sensible à la pile de rendu. Pour l’instant, cela ne semble pas fonctionner dans Firefox

    [1] https://js1024.fun/demos/2022/18/readme

    [2] https://gist.github.com/lifthrasiir/1c7f9c5a421ad39c1af19a9c...

    • La clé pour comprendre cette approche, sans avoir besoin d’entrer dans le détail de la manière dont tout a été forcé à rentrer, est la suivante
      On ne peut utiliser que le format de fichier de polices WOFF2, pour lequel Brotli a été conçu à l’origine, mais pour en tirer parti il faut créer un fichier de police complet
      Les navigateurs récents nettoient généralement les polices avec OpenType Sanitizer (OTS), car placer directement dans le système des fichiers de polices non fiables est très dangereux ; il faut donc créer un fichier WOFF2 suffisamment valide pour être accepté par OTS, tout en contenant à l’intérieur la suite d’octets voulue et en permettant de l’extraire
      Après de nombreux échecs, l’idée de se rabattre sur les largeurs de glyphes, c’est-à-dire les advances, encodées comme une suite d’entiers signés sur 2 octets presque sans contraintes, est fantastique
    • Correction : cela fonctionne encore aussi dans Firefox. J’avais simplement oublié que dans Firefox, le niveau de zoom doit être exactement à 100 %
    • Cette technique est vraiment stupéfiante et bien plus cool que mon article. Chapeau
  • Chromium l’a longtemps bloqué, mais zstd arrive désormais aussi sur le Web. Maintenant qu’il est enfin dans Chrome, il ne reste plus qu’à Safari de suivre

    • J’aimerais que tout passe à Zstandard, mais dans ce cas précis, à ma connaissance, Brotli et Zstandard sont à peu près équivalents à consommation mémoire égale côté décompresseur
    • On dirait au moins que c’est dans la liste des choses à faire : https://webkit.org/standards-positions/#position-168
  • Je développe Batch Compress (https://batchcompress.com/en) et, peu après avoir récemment ajouté la prise en charge de WebP, je l’ai défini comme format par défaut
    À ma connaissance, l’outil produisait déjà les JPEG les plus petits parmi les outils de compression web, mais WebP ne faisait qu’environ 50 % de la taille du JPEG. Le passer par défaut peu après l’ajout de la prise en charge a donc été une décision facile
    Comme le site compte pas mal d’utilisateurs, je m’attendais à quelques plaintes après avoir fait de WebP le format par défaut, mais un mois plus tard environ, il n’y a eu qu’une seule question ou plainte liée à WebP
    Désormais, presque tous les outils et navigateurs semblent prendre en charge WebP. Je n’ai vu récemment qu’un seul site web où l’étape suivante était bloquée parce qu’il ne gérait pas correctement l’upload d’images WebP ; aujourd’hui, presque tout le monde le prend bien en charge

    • Si WebP réduit la taille des fichiers de plus de 15 à 20 % par rapport à JPEG, cette économie vient d’une baisse de qualité, pas d’une amélioration de la compression
      Avec un JPEG bien compressé et optimisé, on ne devrait pas être très loin de WebP
      On peut toujours créer un WebP qui paraît presque identique à un JPEG et réduire la taille du fichier, mais on peut faire la même chose en recompressant vers un JPEG qui paraît presque identique
      C’est une propriété de tous les codecs de compression avec perte et, comme la taille des fichiers augmente de façon exponentielle à mesure que la qualité monte, les gens sont toujours surpris de voir qu’une minuscule baisse de qualité presque invisible peut changer énormément la taille du fichier
    • Je me demande sur quel indicateur de comparaison de qualité repose l’affirmation selon laquelle WebP ferait environ 50 % de la taille de JPEG
      WebP avait mauvaise réputation à cause de paramètres par défaut catastrophiques qui massacraient les détails dans les zones sombres
  • En regardant le source, j’ai remarqué qu’il manquait un espace dans la déclaration doctype. La forme actuelle est incorrecte et devrait contenir un espace

  • J’ai déjà utilisé cette astuce par le passé. Curieusement, je ne me souviens plus à quoi elle m’a servi, mais je crois que c’était probablement pour voir si c’était possible, et j’avais aussi laissé un commentaire ici : https://gist.github.com/gasman/2560551?permalink_comment_id=...
    J’ai aussi retrouvé un vieux prototype, qui semble n’avoir été qu’un test : https://retr0.id/stuff/bee_movie.webp.html

    • Cette page casse mon extension de gestes à la souris
      Aujourd’hui, ce genre de choses qui injectent des scripts s’appellent des extensions plutôt que des add-ons, mais en tout cas l’approche consistant à transmettre d’abord des « déchets », puis à ajouter du JS derrière pour le renvoyer vers la page, est intéressante
      Le passionné de sécurité en moi se demande si cela pourrait devenir exploitable avec des données fournies par l’utilisateur, comme un formulaire de commentaires
      Peut-être que quelqu’un pourrait trouver une séquence d’octets à mettre dans un commentaire qui, après compression, se transformerait en balise script placée avant mon script et exécutée
    • D’après mon expérience, WebP n’était pas bien adapté aux cas généraux où cette technique est réellement utile, c’est-à-dire les données de moins de 10 Ko
      La majeure partie de ce que le WebP sans perte apporte par rapport au PNG relève de la modélisation plutôt que de l’encodage, et ce type de compression de texte n’utilise que la partie encodage de WebP
  • Retirer Google Fonts améliorerait aussi un peu le temps de chargement de la page. Elles sont chargées depuis un serveur distant et nécessitent une poignée de main supplémentaire

    • Mais si suffisamment d’autres sites utilisent aussi cette police, elle pourrait déjà être en local
  • Cette page est cassée au moins dans le navigateur de Sailfish OS. Il y a un long espace vide après le paragraphe suivant
    “Alright, so we’re dealing with 92 KiB for gzip vs 37 + 71 KiB for Brotli. Umm…”
    Cela dit, le surcoût de la compression HTML avec gzip et Brotli n’est rien comparé à la quantité de JS, d’images et de vidéos utilisée par les sites web modernes

    • Même chose dans Orion, Safari et LibreWolf. C’est une page réservée à Chrome ?
    • Même chose dans Mull
  • Personnellement, je n’aime pas beaucoup ce format. Quand j’enregistre une image et qu’elle est enregistrée en WebP, rien ne le prend en charge à part les navigateurs web, donc je dois la convertir avant de pouvoir la modifier ou l’utiliser de façon utile
    J’ai juste l’impression qu’on m’impose une étape supplémentaire

    • Ironie du sort, même Slides, un produit Google, ne prend pas en charge les images WebP
      Cela dit, si la prise en charge continue de s’étendre, ça ira. Je peux supporter qu’un nouveau format apparaisse tous les 20 ans
      .webm peut disparaître, en revanche
    • La conversion prend deux secondes. Sur macOS, c’est littéralement dans le menu du clic droit, et comme la taille est plus petite, ce n’est pas vraiment un problème