Aller au contenu
OneKitly

Ce qu'un minifieur peut retirer, et ce qu'il ne doit pas toucher

Publié le 19/05/2025 · 19 min de lecture · Outils pour développeurs

Daniel Okonkwo

Daniel OkonkwoDéveloppeur front-end et rédacteur Tech chez OneKitly

Performance web · Formats de fichiers

Vérifié à partir de 6 sources

Voir le profil
En bref

Un minifieur ne peut faire que des transformations qui préservent la sémantique, et les cas difficiles sont tous des espaces qui ne sont pas décoratifs. En CSS, l'espace de « div p » est un combinateur de descendance : le retirer fait correspondre le sélecteur à tout autre chose. Les espaces autour de >, + et ~ peuvent disparaître — esbuild a transformé « .card > footer » en « .card>footer » en laissant « .card .card-title » intact. Dans calc(), les espaces autour de + et − sont obligatoires : Chrome rapporte CSS.supports pour calc(100% - 2px) à true et pour calc(100%-2px) à false, et affecter le second laisse la propriété vide. Les espaces autour de * et / sont facultatifs. En HTML, l'espace entre éléments en ligne est du contenu rendu — deux spans mesurent 36,92 px avec un espace entre eux et 27,28 px sans, un décalage de 9,64 px — et il est pleinement significatif dans pre et textarea. En JavaScript, l'insertion automatique de point-virgule fait que joindre des lignes change le comportement : une fonction dont le return est seul sur sa ligne renvoie undefined jusqu'à ce qu'on joigne, moment où elle renvoie l'objet. Et le renommage s'arrête à la frontière des chaînes : le mangling de propriétés a transformé une lecture qui marchait en undefined. Quant au gain, sur les ressources de cette page brotli seul économisait 66,2 % et minifier d'abord n'ajoutait que 18,1 %.

La minification doit préserver le sens, et l'intéressant, ce sont les espaces qui portent du sens : le combinateur de descendance, les espaces dans calc(), l'écart entre deux éléments en ligne. Mesuré ici sur de vrais fichiers, y compris ce que brotli te faisait déjà gagner.

La seule règle : la sortie doit se comporter à l'identique

La minification est une passe de compilation avec un seul contrat : la sortie doit être observationnellement identique à l'entrée. Pas semblable, pas assez proche — identique dans tout comportement dont une page peut dépendre. Tout ce que fait un minifieur en découle, et tout bogue de minification est un endroit où quelqu'un a supposé qu'un octet était décoratif alors que la spécification dit qu'il est une donnée.

Cette distinction explique pourquoi les minifieurs à expressions régulières sont dangereux et les minifieurs à analyseur ne le sont pas. Un outil qui supprime les espaces par motif ignore si un espace donné sépare deux jetons pour la lisibilité ou joint deux jetons en un sens composé. Un outil qui découpe l'entrée selon le module CSS Syntax ou la grammaire ECMAScript, construit un arbre et le réémet ne peut pas commettre l'erreur : au moment où il imprime quoi que ce soit, le sens est déjà figé dans l'arbre.

Tout ce qui suit a été exécuté et non récité. Les échantillons ont été minifiés avec esbuild, les tailles mesurées avec zlib de node en gzip niveau 9 et brotli qualité 11, et les affirmations CSS et de mise en page vérifiées dans Chrome sans interface.

CSS : l'espace qui est un combinateur

Dans un sélecteur, l'espace entre deux sélecteurs composés est le combinateur de descendance. « div p » sélectionne tout p dans un div ; « divp » sélectionne un type d'élément inexistant, et « .card .card-title » sélectionne un .card-title dans un .card tandis que « .card.card-title » sélectionne un élément portant les deux classes. L'espace est un jeton, pas de la mise en forme, et aucun minifieur correct ne le retire.

Les trois autres combinateurs sont de la ponctuation et leurs espaces alentour sont gratuits. En donnant à esbuild les quatre formes « div p », « div>p », « div + p » et « div ~ p », il a renvoyé exactement « div p », « div>p », « div+p » et « div~p ». L'espace de descendance a survécu, pas ceux autour de >, + et ~, car ces caractères sont sans ambiguïté à eux seuls. Dans la feuille d'exemple, la même chose est arrivée aux règles réelles : « .card .card-title » est passé intact tandis que « .card > footer », « .card + .card » et « .card ~ .aside-note » se sont resserrés.

Le cas des règles-at est plus subtil et mérite qu'on s'y arrête. La requête média « @media (min-width: 600px) and (max-width: 900px) » est ressortie en « @media(min-width:600px)and (max-width:900px) ». L'espace avant « and » a disparu, car une parenthèse fermante termine déjà le jeton précédent. L'espace après « and » est resté, car « and( » se découperait en jeton de fonction plutôt qu'en identifiant suivi d'une parenthèse. Toute la discipline tient en une ligne : un espace est retirable exactement quand les deux jetons de part et d'autre ne peuvent pas fusionner en un jeton différent.

CSS : les espaces à l'intérieur des valeurs

calc() est l'exemple le plus net, car l'exigence est asymétrique. La spécification CSS Values impose des espaces des deux côtés de + et −, faute de quoi un jeton comme « -2px » se découpe en une seule dimension négative et l'expression perd son opérateur. Chrome le confirme précisément : CSS.supports pour width et calc(100% - 2px) renvoie true, tandis que calc(100%-2px), calc(100% -2px) et calc(100%- 2px) renvoient tous false. Affecter style.width = « calc(100%-2px) » laisse la propriété à une chaîne vide, car la déclaration entière est écartée comme invalide.

Les opérateurs de multiplication et de division n'ont pas ce problème, et le navigateur le confirme : calc(100%*2) et calc(100%/2) renvoient tous deux true. Un minifieur qui comprend la grammaire pourrait donc resserrer ces deux-là et pas les autres. En pratique, esbuild est prudent et a conservé chaque espace de « calc(100% - 2 * var(--gap)) » — correct pour le moins, et une petite occasion manquée pour l'étoile.

Deux autres catégories d'espaces intouchables sont apparues dans la même exécution. Les valeurs de chaîne sont littérales : la déclaration content: « new » a conservé ses deux paires d'espaces intérieurs, car ces caractères sont insérés dans le document. Et les propriétés personnalisées sont des flux de jetons plutôt que des valeurs analysées : esbuild a donc laissé « --card-bg: #ffffff » avec l'espace après son deux-points tout en retirant l'espace identique de « color: var(--card-fg) ». Le reste de l'échantillon montre ce qu'un minifieur gagne quand il comprend la grammaire des valeurs : rgba(0, 0, 0, 0.08) est devenu #00000014, #0000ff est devenu #00f, 150ms est devenu .15s, opacity 0.7 est devenu .7, margin: 0 0 8px 0 est devenu margin:0 0 8px, et ::after est devenu :after.

HTML : l'espace entre éléments en ligne est du contenu

Le traitement des blancs en CSS réduit une suite d'espaces en flux normal à un espace unique — mais un espace, pas rien. Entre deux éléments de niveau ligne, cet espace est rendu et occupe de la largeur : le supprimer déplace la mise en page. Mesuré dans Chrome sans interface en monospace 16 px, deux spans adjacents séparés par un saut de ligne dans la source se terminaient à x = 36,92, tandis que les deux mêmes spans écrits sans blanc entre eux se terminaient à x = 27,28. L'écart de 9,64 px est exactement un caractère espace, et c'est la différence entre une rangée de liens qui lit « one two three » et une qui lit « onetwothree ».

C'est pourquoi les minifieurs HTML agressifs sont configurables et pourquoi leurs réglages par défaut sont généralement prudents. Réduire cinq espaces et deux sauts de ligne à un espace est toujours sûr en flux normal. Supprimer le dernier espace restant entre deux boîtes en ligne ne l'est pas, et un minifieur qui le fait par règle générale reflue silencieusement menus, fils d'Ariane, listes d'étiquettes et icônes en ligne. Tout outil qui propose de retirer les blancs entre balises propose de changer ta mise en page contre des octets.

Deux éléments sont absolument interdits : pre et textarea. Les deux valent par défaut white-space: pre, donc chaque espace, tabulation et saut de ligne à l'intérieur est conservé et rendu. La page d'exemple contient un bloc de code indenté et un textarea aux espaces initiaux significatifs, et le réducteur de blancs utilisé pour la mesure a dû recevoir une exception explicite pour les deux. Tout minifieur sans cette exception détruit silencieusement les exemples de code et les champs préremplis. Le même soin vaut à l'intérieur des éléments script et style, et pour le saut de ligne initial juste après une balise pre ouvrante, que l'analyseur HTML supprime par spécification — subtilité qui rend l'outillage artisanal faux dans les deux sens.

JavaScript : insertion automatique de point-virgule et renommage

ECMAScript insère des points-virgules à certains sauts de ligne, ce qui rend un saut de ligne porteur de sens. Le cas canonique est un return seul sur sa ligne. Exécuter function f(){ return \n { ok: true } } a renvoyé undefined, car un point-virgule est inséré juste après return. Écrire le même code sur une ligne a renvoyé { ok: true }. Un minifieur naïf qui joint les lignes change donc la valeur produite par la fonction. L'inverse mord aussi : l'extrait let x = 1 \n ++x évalue x à 2, tandis que joindre ces deux lignes lève SyntaxError: Invalid left-hand side expression in postfix operation.

Un minifieur à analyseur ne peut commettre ni l'une ni l'autre erreur : au moment où il imprime, le point-virgule est déjà décidé. En donnant la même fonction au return isolé à esbuild, on obtient function t(){}export const r=void 0; — il a conservé la sémantique, vu que la fonction ne pouvait renvoyer que undefined, et replié l'appel en void 0. C'est la différence entre une transformation de texte et un compilateur.

L'autre danger JavaScript est le renommage, et sa frontière est exacte : un minifieur peut renommer tout ce dont il voit chaque référence, et rien d'autre. Les variables locales et les paramètres de fonction sont éligibles, et c'est de là que vient l'essentiel du gain. Les noms de propriétés d'objet ne le sont pas, car une propriété peut être atteinte par une chaîne que le minifieur ne peut suivre. Démonstration : un module renvoyant [config.userName, o[« userName »], o[« retryCount »]] a donné [« ada », « ada », 3] en minification simple, et [« ada », undefined, undefined] une fois le mangling de propriétés activé. L'accès par point a été renommé avec la définition ; les deux lectures par chaîne demandaient toujours les anciens noms et n'ont rien trouvé. Le même piège attrape tout ce qui est atteint par nom à l'exécution — accès entre crochets construit depuis une variable, allers-retours JSON, liaisons de framework, et eval direct.

Mesuré : ce que vaut la minification après compression

Minification et compression retirent une redondance qui se recoupe : celle qui passe en second paraît toujours moins impressionnante. Sur les échantillons écrits à la main : le CSS est passé de 1 434 à 1 066 octets, soit 25,7 % de moins, mais après brotli la paire faisait 552 contre 459 — seulement 16,8 %. Le JavaScript est passé de 1 518 à 747 octets, 50,8 % de moins, mais 552 contre 395 après brotli, 28,4 %. Le HTML est passé de 1 033 à 782 octets, 24,3 %, et 322 contre 302 après brotli — 6,2 %, soit vingt octets.

Réunir les trois en une charge de page pose le chiffre honnête sur la table. Les ressources brutes totalisent 3 985 octets et brotli les ramène à 1 346 — 66,2 % d'économie par la seule compression, sans étape de build. Minifier d'abord puis compresser donne 1 102 octets. La contribution marginale de la minification, par-dessus une couche de compression que tu as déjà, est donc de 244 octets : 18,1 %. Réelle et bonne à prendre, mais un ordre de grandeur en dessous de ce que suggère le chiffre en octets bruts.

Quatre fichiers réels de ce dépôt montrent à quel point la réponse dépend du contenu. globals.css a fondu de 68,2 % brut et 70,2 % après brotli — spectaculaire, et entièrement expliqué par le fait que 2 084 de ses 3 393 octets sont des commentaires, les treize blocs de règles restant intacts des deux côtés. app-shell.module.css, surtout composé de déclarations réelles, a donné 4,4 % brut et 5,8 % après brotli. Deux modules TypeScript riches en contenu ont donné 10,8 % et 6,5 % brut, mais seulement 3,4 % et 2,3 % après brotli, car un fichier surtout fait de chaînes littérales ne contient presque rien qu'un minifieur ait le droit de toucher. Règle empirique : la minification paie proportionnellement à la part de commentaires, d'indentation et de longs identifiants locaux, et ne paie rien sur des données.

Un ordre des opérations qui marche

Active d'abord la compression : c'est le plus gros gain unique, elle ne demande aucune étape de build et ne peut rien casser. Sur ces échantillons, brotli seul a retiré 66,2 % des octets. Minifie ensuite avec un outil à analyseur pour chaque langage, à ses réglages par défaut. Puis, seulement si tu as une raison mesurée, va chercher les options agressives — mangling de propriétés, suppression des blancs entre balises — et traite chacune comme une modification à tester, car chacune est un endroit où le contrat de préservation sémantique a été délibérément relâché.

Deux habitudes valent plus que n'importe quel réglage de minifieur. Retire commentaires et code mort à la source plutôt que de compter sur le minifieur pour les remarquer — globals.css était à 61 % de commentaires, et ce seul fait explique l'intégralité de sa réduction de 68 %. Et vérifie la sortie, pas la promesse : fais passer le paquet minifié dans ta suite de tests, et compare les tailles compressées des deux côtés plutôt que les brutes, car le chiffre brut est celui qui flatte et le chiffre compressé celui que tes utilisateurs téléchargent vraiment.

Transformations passées dans esbuild sur la feuille et le module d'exemple, avec le verdict pour chacune
AvantAprèsSûr ?Pourquoi
.card > footer.card>footerOuiLe > est sans ambiguïté à lui seul ; les espaces ne portent rien
.card .card-title.card .card-title (inchangé)À ne pas toucherL'espace est le combinateur de descendance ; le retirer sélectionne un élément portant les deux classes
calc(100% - 2px)calc(100% - 2px) (inchangé)À ne pas toucherChrome déclare calc(100%-2px) non pris en charge et écarte la déclaration
content: « new »content:« new » (espaces conservés)À ne pas toucherLe contenu des chaînes est inséré tel quel dans le document
rgba(0, 0, 0, 0.08)#00000014OuiL'hexadécimal à huit chiffres représente exactement la même couleur
margin: 0 0 8px 0margin:0 0 8pxOuiLe raccourci reprend la deuxième valeur quand la quatrième est omise
return seul sur sa lignesaut de ligne retiré par un outil texteNonLa fonction renvoyait undefined avant et l'objet après
o.userName et o[« userName »]le mangling de propriétés ne renomme que le premierNonLe résultat mesuré est passé de [ada, ada, 3] à [ada, undefined, undefined]
Compresseur CSSMinifie le CSS en retirant commentaires et espaces pour réduire la taille de ta feuille de style.Essayer l'outil

Questions fréquentes

Si mon serveur envoie déjà gzip ou brotli, faut-il encore minifier ?
Oui, mais attends-toi à un gain bien plus petit que ne le suggèrent les chiffres bruts, et fais d'abord marcher la compression. Mesuré sur la page d'exemple, brotli seul a ramené 3 985 octets de ressources brutes à 1 346 — 66,2 % économisés sans aucune étape de build. Minifier avant de compresser a atteint 1 102 octets : la contribution marginale de la minification était donc de 244 octets, soit 18,1 % de plus. Cela vaut la peine, et cela ne coûte rien par requête une fois que ton build s'en charge. Les deux se recoupent parce qu'ils attaquent la même redondance : identifiants longs répétés, suites d'indentation et texte de commentaires sont exactement ce qu'un compresseur à dictionnaire élimine le mieux. Là où la minification gagne franchement, c'est sur ce que la compression ne peut pas faire, parce que c'est sémantique et non textuel — élimination de code mort, repliement de constantes, suppression de branches inatteignables, raccourcissement de la syntaxe des couleurs et des unités. L'ordre qui compte : compression activée, puis minification, puis mesure des tailles compressées et non des brutes.
Ma mise en page a bougé après avoir activé la minification HTML. Pourquoi ?
Presque certainement parce que le minifieur a retiré des blancs entre éléments de niveau ligne, qui sont du contenu rendu et non de la mise en forme. Le traitement CSS des blancs réduit une suite d'espaces et de sauts de ligne en flux normal à un espace — un, pas zéro — et cet espace survivant occupe de la largeur entre deux boîtes en ligne. Mesuré dans Chrome sans interface en monospace 16 px, deux spans séparés par un saut de ligne dans la source se terminaient à x = 36,92, et la même paire sans aucun blanc à x = 27,28 : 9,64 px d'écart, exactement un espace. Sur une barre de navigation, une liste d'étiquettes ou une rangée de liens en ligne, cet écart saute aux yeux et passe pour un bogue. Cherche une option du genre collapseWhitespace avec un mode agressif ou prudent, et préfère le prudent. Si tu ne veux aucun écart entre deux éléments en ligne, supprime-le en CSS avec un conteneur flex ou grid, ou via font-size sur le parent, pour que le balisage reste indépendant de la mise en page.
Est-il parfois sûr de renommer les propriétés d'objet ?
Seulement si tu garantis que chaque accès à la propriété est visible du minifieur, ce qui suppose en pratique d'adopter une convention de nommage et d'en informer l'outil. Le motif courant est de ne manger que les propriétés correspondant à un motif — un tiret bas final, par exemple — pour que les champs internes soient renommés et tout le public intact. Ce qui casse : tout accès par chaîne. Démontré ici : un module renvoyant [config.userName, o[« userName »], o[« retryCount »]] a donné [« ada », « ada », 3] en minification ordinaire et [« ada », undefined, undefined] une fois le mangling de propriétés activé — l'accès par point a suivi la définition, pas les deux lectures par chaîne. Le même mode de défaillance couvre l'accès entre crochets construit depuis une variable, les clés venues de JSON, les gabarits de framework qui lient par nom, et tout ce qui est parcouru avec Object.keys. Comme la casse est silencieuse et n'apparaît que sur le chemin de code qui utilise la chaîne, traite le mangling de propriétés comme une optimisation qui exige une campagne de tests complète, pas comme une case à cocher.
Peut-on écrire un minifieur avec des expressions régulières ?
Tu peux écrire quelque chose qui marche généralement, ce qui est le pire résultat possible, car les échecs sont rares et silencieux. Un motif qui supprime des suites de blancs ne distingue pas un combinateur de descendance d'une indentation, ne sait pas que l'espace avant un moins dans calc() est grammaticalement requis, ne voit pas qu'un saut de ligne avant une accolade fermante fait qu'un return renvoie undefined, et ne sait pas que les caractères entre guillemets sont du contenu. Chacune de ces distinctions exige de découper l'entrée selon la vraie grammaire. Le réducteur de blancs utilisé pour la mesure HTML de cet article est délibérément naïf, et il a fallu lui tailler une exception explicite pour pre et textarea avant qu'il ne produise une sortie correcte — et il ne serait toujours pas sûr sur une page contenant des scripts en ligne avec des chevrons dans des chaînes. La réponse pratique : utilise un outil à analyseur par langage et dépense tes efforts sur l'entrée — moins de commentaires livrés, pas de code mort, des noms locaux plus courts là où cela ne nuit pas à la lisibilité.
Pourquoi minifier mon fichier riche en données n'a-t-il presque rien donné ?
Parce qu'un minifieur n'a le droit de toucher qu'à la syntaxe, et qu'un fichier de données est presque entièrement du contenu. Les chaînes littérales doivent survivre octet pour octet, les clés d'objet utilisées à l'exécution ne peuvent pas être renommées, et les nombres sont déjà aussi courts que possible. Les mesures de cet article montrent nettement le schéma : deux modules TypeScript riches en contenu de ce dépôt ont fondu de 10,8 % et 6,5 % brut, mais de seulement 3,4 % et 2,3 % après brotli, car le peu que la minification a retiré, c'était de l'indentation et de la ponctuation que le compresseur allait de toute façon écraser. Compare avec globals.css, qui a fondu de 68,2 % — entièrement parce que 2 084 de ses 3 393 octets étaient des commentaires. Si un fichier de données est vraiment gros, le levier n'est pas la minification mais le format et la livraison : passe les données derrière une API pour que les pages ne récupèrent que ce qu'elles affichent, découpe-les pour qu'une route charge sa propre tranche, ou sors-les du paquet JavaScript vers du JSON que le navigateur analyse plus vite et met en cache séparément.

Articles qui pourraient t'intéresser

Tous les guides
GuideEmbellir ou minifier : à quoi sert chacun, et ce que ça change au poidsQuatre vraies feuilles de style passées au minifieur, mesurées brutes puis après gzip. Retirer toutes les espaces a fait gagner 48, 103, 104 et 147 octets compressés ; retirer les commentaires, 57, 1 358, 2 420 et 1 042. Et les cinq entrées que ce minifieur casse.TutorielHEX vs RGB : lire et convertir les couleursHEX et RGB sont deux notations des mêmes couleurs. Voici comment lire chacune, convertir de l'une à l'autre et ajouter de la transparence.ExplicationDégradés, banding, et pourquoi le milieu paraît terneInterpoler en sRGB revient à moyenner des nombres encodés en gamma : le milieu de rouge à vert vaut #808000 alors que la réponse à mi-lumière est #bcbc00 — 57,2 % de lumière en moins. Le banding est un autre problème d'arithmétique : 8 bits donnent 256 paliers, et un dégradé sombre peut n'en avoir que 28. Voici les deux, calculés.ExplicationLes harmonies de couleurs sont de la géométrie sur une roue — et la roue est fausseComplémentaire, triadique, analogue et complémentaire divisée ne sont que des rotations : ajoute 180°, 120°, 30° ou 150° à une teinte. L'arithmétique est triviale. Le problème, c'est que le cercle de teintes HSL n'est pas perceptuellement uniforme — le jaune et le bleu à la même clarté HSL diffèrent d'un facteur 12,85 en luminance — donc une palette générée doit être vérifiée en contraste ensuite.ExplicationRapport de contraste : comment le WCAG le calcule vraimentLe rapport WCAG vaut (L1 + 0,05) ÷ (L2 + 0,05), et L est la luminance relative, pas la luminosité. Le vert en porte 71,52 % et le bleu 7,22 %, d'où le bleu pur sur blanc qui passe à 8,59:1 tandis que le gris moyen échoue à 3,95:1. Voici tout le calcul, exécuté de bout en bout.ExplicationComment marchent vraiment les polices Instagram (ce ne sont pas des polices)Rien n'est mis en forme. Chaque lettre est remplacée par un autre caractère Unicode qui ressemble à du gras ou du cursif — d'où un lecteur d'écran qui lit du charabia et des appareils qui affichent des carrés vides.

Outils similaires

Sources

Tu as repéré une erreur dans cet article ?