Skip to content

Une petite histoire de compression

Published: at 12:00 AM

J’ai développé une petite application open source de poker planning : poker.slashgear.dev. Le code source est disponible sur GitHub.

La stack initiale était composée de React 19, TypeScript, TanStack Router, TanStack Query et Tailwind 4, le tout servi par HonoJS. Un jour, en regardant le poids de l’application, je me suis dit que c’était beaucoup trop pour une simple app de poker planning.

Un repère qui m’a marqué

“Quand j’ai commencé le web, une bonne page web devait faire 100 Ko maximum” — Pascal Martin

Cette phrase m’a marqué. Une application de poker planning, c’est un écran avec des cartes et un bouton. Est-ce qu’on a vraiment besoin de plus de 100 Ko pour ça ?

Spoiler : la v2.16.0 dépassait largement ce seuil.

L’état initial : v2.16.0

En analysant le bundle avec rollup-plugin-visualizer, le constat était clair :

Pour une app qui affiche des cartes de poker et synchronise un état simple, c’était beaucoup trop.

Étape 1 : Drop React pour Preact

Ce qui m’a mis la puce à l’oreille, c’est le travail de Julien Sulpis qui a publié js-frameworks-bundle-benchmark, une comparaison du poids des frameworks front modernes. Les chiffres sont édifiants : Preact est drastiquement plus léger que React.

La première question à se poser : ai-je vraiment besoin de React ?

Preact est un drop-in replacement de React qui pèse une fraction du poids original. Rien dans l’application ne justifiait les fonctionnalités spécifiques à React 19.

J’en ai profité pour :

Commit de référence : 0f29c59

Résultat :

Étape 2 : Compression au build time

L’application est une SPA full statique. Les fichiers ne changent jamais entre deux déploiements. Alors pourquoi les compresser à chaque requête ?

Comme le rappelle l’almanac HTTP Archive, la compression au build time permet d’utiliser les niveaux de compression les plus élevés sans impact sur le temps de réponse.

J’ai mis en place 3 formats de pré-compression :

Voici l’extrait du Dockerfile :

RUN find /app/dist/client -type f \( -name "*.js" -o -name "*.css" -o -name "*.html" -o -name "*.svg" \) \
    -exec brotli --best {} \; \
    -exec zstd --ultra -22 {} \; \
    -exec zopfli --i100 {} \;

Côté serveur, il suffit de configurer le serving de fichiers pré-compressés. Zéro CPU runtime pour la compression.

Dans mon cas j’utilise HonoJS avec l’option precompressed: true, mais cette approche fonctionne avec n’importe quel serveur web.

Exemple avec Nginx

Activez le module ngx_http_gzip_static_module pour servir les fichiers .gz pré-compressés, et le module ngx_http_brotli_static_module pour les .br :

server {
    # Sert les .gz pré-compressés au lieu de compresser à la volée
    gzip_static on;

    # Sert les .br pré-compressés (module brotli)
    brotli_static on;

    location /assets/ {
        root /var/www/html;
    }
}

Exemple avec Apache

Avec les modules mod_deflate et mod_rewrite, Apache peut servir les fichiers pré-compressés :

# Servir les fichiers .br si le navigateur supporte Brotli
RewriteEngine On
RewriteCond %{HTTP:Accept-Encoding} br
RewriteCond %{REQUEST_FILENAME}.br -f
RewriteRule ^(.*)$ $1.br [L]

# Servir les fichiers .gz si le navigateur supporte gzip
RewriteCond %{HTTP:Accept-Encoding} gzip
RewriteCond %{REQUEST_FILENAME}.gz -f
RewriteRule ^(.*)$ $1.gz [L]

# Indiquer le bon Content-Type et Content-Encoding
<FilesMatch "\.br$">
    Header set Content-Encoding br
    RemoveLanguage .br
</FilesMatch>
<FilesMatch "\.gz$">
    Header set Content-Encoding gzip
</FilesMatch>

Exemple avec Caddy

Caddy gère nativement les fichiers pré-compressés avec la directive file_server :

example.com {
    root * /var/www/html
    file_server {
        precompressed zstd br gzip
    }
}

C’est tout. Caddy cherche automatiquement les fichiers .zst, .br et .gz et les sert s’ils existent.

FormatNiveauJS bundle
GZIP 6 (à la volée)Standard~21.5 Ko
Zopfli (build time)Optimal GZIP~20.8 Ko
Brotli 11 (build time)Optimal~18.9 Ko
Zstd 22 (build time)Ultra~19.1 Ko
Taille JS bundle par format de compression GZIP 6 : 21.5 Ko, Zopfli : 20.8 Ko, Brotli 11 : 18.9 Ko, Zstd 22 : 19.1 Ko. Brotli offre le meilleur ratio. Taille JS bundle par format de compression (Ko) 21.5 20.8 18.9 19.1 GZIP 6 Zopfli Brotli 11 Zstd 22

Étape 3 : Optimisation Tailwind CSS

Tailwind 4 utilise par défaut des CSS custom properties pour les couleurs. En ajoutant theme(inline) dans l’import, les couleurs sont directement inlinées dans les classes utilitaires :

@import "tailwindcss" theme(inline);

J’ai aussi supprimé une classe .sr-only qui était dupliquée (déjà fournie par Tailwind).

Résultat : CSS ~27.78 Ko → ~21.51 Ko (-22%)

Récapitulatif : v2.16.0 vs v3.2.0

Métriquev2.16.0v3.2.0Gain
JS bundle (gzip)~95 Ko~21 Ko-78%
CSS bundle~27.78 Ko~21.51 Ko-22%
Build time~2.1s~0.6s-72%
Comparaison des bundles v2.16.0 vs v3.2.0 JS bundle gzip : 95 Ko en v2.16.0 contre 21 Ko en v3.2.0 (-78%). CSS bundle : 27.78 Ko contre 21.51 Ko (-22%). Taille des bundles v2.16.0 vs v3.2.0 (Ko) v2.16.0 v3.2.0 JS 95 Ko 21 Ko -78% CSS 27.78 Ko 21.51 Ko -22% Build 2.1s 0.6s (-72%)

Pour aller plus loin

La conférence d’Hubert Sablonnière et moi-même sur la compression web est une mine d’or sur le sujet :

L’article 24 jours de web 2024 détaille aussi les mécanismes de compression HTTP.

Ce qu’il faut retenir


Next Post
Managing Terraform Modules with Nx Monorepo