PNG2CSS : un outil pour théoriquement accélerer le chargement des pages

Vous me connaissez, j’apporte un intérêt assez particulier à toutes les techniques permettant d’accélérer le chargement des pages web, ce dans le but d’éviter d’être (entre-autres) pénalisé dans l’algorithme de ranking de Google pour trop grande lenteur. Dans toutes les techniques utilisées, il y en a une qui consiste à diminuer grandement le nombre d’images affichées sur ses pages. En effet, soit on utilise les sprite-CSS, auquel cas on ne charge qu’un nombre très réduit d’images et on en gère l’affichage dans le CSS, soit on charge chaque image séparément, ce qui a pour conséquence d’augmenter d’autant le nombre de requêtes faites par le navigateur pour aller chercher toutes les ressources nécessaire à l’affichage complet de la page.

Le problème, c’est que pour toutes les petites images à charger dans une page, beaucoup son petites, mais nécessitent chacune une requête au serveur qui les fournit. Or, très souvent, c’est la connexion et le temps de latence au serveur qui prennent davantage de temps que le téléchargement lui-même ! C’est la raison pour laquelle on utilise le CSS pour créer la mise en forme, car le CSS permet de « générer des images » : plutôt que de télécharger l’image toute faite, on délègue sa création à l’ordinateur de l’utilisateur. Par exemple : plutôt que de faire télécharger un background de page de 2000×1500 pixels de couleur rouge unie qui en PNG prendrait 44ko (j’ai fait le test), on peut le remplacer par une ligne CSS disant :

div.maclasse{
     position:absolute;
     left:0px;
     top:0px;
     width:2000px;
     height:1500px;
     background:RED;
}

..ce qui fait moins d’1 ko : l’ordinateur va générer tout seul l’image.

Pourquoi, donc, ne pas tenter de supprimer le téléchargement de ces images en tentant de les générer à partir du fichier CSS ?

C’est ce que j’ai fait via un script Python qui prend en entrée un fichier PNG et qui fournit en sortie un fichier HTML et un CSS permettant tous deux d’afficher l’image, en la générant pixel par pixel.

Comment ça marche ?

Le programme prend un fichier PNG, en extrait les données. Cela donne (je simplifie) un grand tableau à double entrée avec, pour chaque case, la couleur de la case (ou du pixel). Après, il suffit d’afficher ce pixel dans la page HTML en lui donnant comme couleur de fond la couleur qu’il avait dans l’image. Le programme reconstruit donc l’image à l’aide du CSS.

Attention, il n’est nullement question d’alléger les fichiers pour l’instant : par exemple, la compression du logo Google pris en PNG (qui faisait 12ko) a donné un HTML de 851ko et un CSS de 2650ko. Donc le script n’optimise pas en terme de taille, il optimise en terme de nombre de fichiers.

Plusieurs passes d’optimisation

Comme je vous l’ai dit plus haut, le script prend chaque pixel de l’image source, et le transforme en une ligne CSS, fournissant le background pour un pixel, et la position à laquelle afficher le pixel. Bien sûr, je récupère proprement la position du pixel dans l’image source (ou je la calcule, plus précisément), et j’adapte la position à laquelle je veux afficher le pixel dans la page cible.

Par exemple pour le pixel tout en haut à gauche de l’image :

div.maclasse1{
position:absolute;
     left:0px;
     top:0px;
     width:1px;
     height:1px;
     background:RED;
}

Explication : le pixel est décalé de 0 pixel de la gauche de l’écran (left:0px), de 0 pixel du haut de l’écran (top:0px), et il a une largeur de 1 pixel (width:1px) et une hauteur de 1 pixel (height:1px).

Mais cette technique n’est pas du tout optimisée : elle créée autant de lignes CSS (et de ligne HTML) que de pixels dans l’image. Donc pour une image source de 200×300 pixels, vous vous retrouvez avec des fichiers HTML et CSS de 60000 lignes chacun. Et ce, même si l’image n’avais qu’une seule couleur.

L’idée des passes d’optimisation (que l’on retrouve souvent dans les algorithmes de compression), c’est de revoir plusieurs fois la sortie de l’image, et donc, quand on a des informations similaires, de les regrouper. Par exemple : si j’ai trois pixels de la même couleur les uns à côté des autres, plutôt que d’écrire trois lignes CSS (une par pixel) :

div.maclasse1{
     position:absolute;
     left:0px;
     top:0px;
     width:1px;
     height:1px;
     background:RED;
}
div.maclasse2{
     position:absolute;
     left:1px;
     top:0px;
     width:1px;
     height:1px;
          background:RED;
}
div.maclasse3{
     position:absolute;
     left:2px;
     top:0px;
     width:1px;
     height:1px;
     background:RED;
}

…pourquoi ne pas écrire une seule ligne disant qu’il faut générer les 3 à la suite ?

div.maclasse1{
     position:absolute;
     left:0px;
     top:0px;
     width:3px;
     height:1px;
     background:RED;
}

Comme vous pouvez le voir, dans celui-là, je dis de placer un background de largeur 3 pixels (width:3px). Le résultat affiché est exactement le même dans les deux cas, mais celui en bas prend trois fois moins de places en terme d’information.

L’idéal serait donc d’optimiser les fichiers de sortie afin de rassembler où c’est possible les directives, ce pour gagner de la place (et ne pas avoir des fichiers en sortie avec des milliers de lignes). Le problème, c’est que l’image source nous donne des informations pixel par pixel : il faut donc passer plusieurs fois pour détecter les pixels similaires et les associer entre eux.

Autant de passes que de dimensions

Attention, ici je parle de Run-Length Encoding (codage par plage) : je vous conseille d’aller lire la (très courte) fiche Wikipédia sur le sujet.

Plus exactement, le script fait 2 passes : dans la première, il transforme ce qu’il reçoit (des triplets RGB) en couleurs HTML (hexadécimales), tout en détectant les pixels qui se suivent et sont de la même couleur (sur une même ligne) : il les associe. Pour le PNG de logo Google, on obtient un HTML de 256ko et un CSS de 789ko.

Cette première passe ne suffit pas : en effet, pour une image de 200×300 pixels de la même couleur, il y aurait quand même 300 lignes pour dire de mettre la même couleur sur toute la ligne. Pourquoi pas ne mettre qu’une seule ligne pour dire de ne mettre la même couleur sur toute la ligne, mais également pour toutes les lignes ? De 300 lignes, on passerait à une seule. C’est le but de la deuxième passe.

(si vous voulez, la première passe fait une optimisation par ligne, la seconde rajoute les colonnes)

Évolutions potentielles

L’avantage du PNG, c’est qu’il gère la transparence. Malheureusement, de ce que j’ai vu sur la librairie que j’utilise (PyPNG), cet aspect semble être fournit, mais je n’ai pas pu le constater sur le fichier que j’ai testé (un bête fichier avec de la transparence, généré avec Paint.NET), et mon script a alors planté lamentablement. Je vous recommande donc de ré-enregistrer vos fichiers avec le Paint de Windows pour être sûr que ça fonctionne (en tout cas, c’est ce que j’ai fait pour chaque fichier PNG de test, et ça fonctionnait).

Par ailleurs, grâce au CSS3, il serait possible (mais très violent) de détecter les dégradés et de les reproduire avec le CSS. Ce serait un gros potentiel d’optimisation, mais j’ai du mal à croire à la faisabilité de la chose.

Enfin, je peux également compresser encore plus en définissant une couleur de base (ce que j’appelle la 3ième passe d’optimisation sur le graphique ci-dessus). Pour cela, je n’ai qu’à prendre la couleur qui revient le plus souvent, et à décider quelle est la couleur de fond. Puis, je saute les passages où c’est cette couleur qui est demandée. Sans l’utilisation de cette technique, j’obtiens, après les deux passes, le fichier HTML a 203ko et le CSS à 631ko pour un logo Google en PNG d’une taille de 12ko. Avec cette technique, j’obtiens une image identique, mais un HTML de taille 189ko et un CSS de 585ko.

Le petit plus rigolo

Un truc rapide et amusant : amusez-vous à grossir la taille du texte, vous verrez que le logo grossi, bien sûr, mais de surcroît vous verrez de quelle façon il espace progressivement les pixels (testé sur Chrome) :

Le code est téléchargeable là.

Pour tester chez vous sur le logo Google, c’est là (l’image originale est là).

Au passage, un programme facile d’utilisation existe déjà pour les jpeg, mais il n’est qu’en noir et blanc, et ne fonctionne pas pixel par pixel : jpeg2css.

10 réflexions sur « PNG2CSS : un outil pour théoriquement accélerer le chargement des pages »

  1. @Kermit: Merci pour l’info (je ne connaissais pas) (honte sur moi). Je vais faire des tests. A priori le DUS peut être très intéressant, mais de ce que j’ai lu sur Wikipédia, il peut y avoir des problèmes de compatibilité (contrairement à la technique du CSS basique présenté ci-dessus).

    RépondreRépondre
  2. Effectivement pour IE<8 il y a un probleme, mais contournable via MHTLM :
    dans le css on duplique l'image (nouvelle perte en taille, mais toujours mieux que du full CSS), une fois en MTHML pour ie, et une fois avec l'URI Data scheme.

    http://www.phpied.com/mhtml-when-you-need-data-uris-in-ie7-and-under/

    Mais si tu veux juste réduire le nombre de fichiers, sans perte de taille, tu a aussi le CSS sprite :
    sur la même image, tu fait un patchwork des images de ta page, et dans le CSS tu definit pour chaque image à quelle portion de l'image originale elle correspond (et là il y a matière à développer des scripts !) :

    les images de google.com :
    http://www.google.com/images/nav_logo8.png

    des explications :
    http://css-tricks.com/css-sprites/

    un générateur :
    http://spritegen.website-performance.org/

    RépondreRépondre
  3. @Kermit: Oui, j’ai vu pour les sprite-css, je l’ai même précisé dans le premier paragraphe de l’article 🙂

    Pour les DUS, l’idée reste bonne, mais IE fait vraiment suer à jamais respecter les standards, ça commence à être lourd.

    RépondreRépondre
  4. Merci énormément pour votre effort, mais je demande si y’en a des nouveauté a propos de cette outils, on est a 2013 plusieurs choses on changer, y’a pas une possibilité d’utiliser le HTML5 et le CSS3 dans la conversion ?
    Merci d’avance, et Bonne continuation.

    RépondreRépondre

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.