Lerm-IT

Blog traitant de technologies informatiques. Logiciel libre, AdminSys, DevOps et GNU/Linux !

19 Apr 2020

[Hugo] Supprimer le CSS inutilisé

Contexte

Il est extrêmement rare aujourd’hui que le design d’un site web ne fasse pas intervenir un framework CSS. Il en existe plusieurs qui sont très reconnus

  • Twitter Boostrap
  • Bulma
  • Meterialize
  • Tailwind
  • etc.

Chacun de ces frameworks CSS ont un poids qui n’est pas négligeable 1. Ainsi un framework comme Twitter Bootstrap dans sa version 4.1.3 représente:

  • 172Ko dans sa version non minifiée (boostrap.css)
  • 140Ko dans sa version minififiée (boostrap.min.css)
  • 24Ko s’il est compressé en gzip

C’est autant de bande passante qui est consommée par chacun de vos visiteurs.

Il se trouve que vous n’utilisez qu’une infime partie des fonctionnalités de ces frameworks CSS. Aussi une grande partie est inutile. Nous allons voir ici commenter supprimer ce code superflu.

Notre exemple se basera sur le générateur de site statique Hugo, mais il est possible d’adapter ce comportement à d’autre système (PHP, JS, etc.)

Suppression du CSS non utilisé

Identifier les ressources à supprimer

Pour pouvoir supprimer les données inutilisées il vous nous être nécessaire de connaitre la liste des class et id utilisés dans nos pages HTML.

Pour cela, la version 0.68.0 de Hugo a ajouté l’option de configuration writeStats 2. Celle-ci permet de générer un fichier hugo_stats.json à la racine de votre projet. Dans ce fichier vous retrouverez des statistiques sur les éléments HTML utilisés dans votre site, à savoir :

  • La liste de toutes les balises HTML utilisées
  • La liste de toutes les classes HTML utilisées
  • La liste de tous les identifiants HTML utilisés

Ce fichier de statistique nous servira à alimenter l’outil de suppression du CSS en lui instruisant la liste des configurations CSS nécessaire (et donc celles qui sont inutiles).

Voici la configuration à mettre dans votre fichier config.toml

[build]
  writeStats = true

À la prochaine génération de votre site (via la commande hugo sans argument) vous trouverez votre fichier hugo_stats.json.

$ hugo

                   | EN
-------------------+------
  Pages            | 346
  Paginator pages  |   1
  Non-page files   |   2
  Static files     |  51
  Processed images |   0
  Aliases          |   2
  Sitemaps         |   1
  Cleaned          |   0

Total in 366 ms
$ ls -l hugo_stats.json
-rw-r--r--    1 1000     1000          8194 Apr 17 07:08 hugo_stats.json

Activer la génération post-génération

PurgeCSS2 est un projet Javascript disponible à l’installation via npm. Celui-ci est responsable de la suppression des ressources inutilisées dans nos fichiers CSS. Nous allons néanmoins devoir télécharger quelques prérequis avant de pouvoir l’utiliser.

À la vue, du processus de génération des sites dans hugo il est nécessaire de modifier un peu l’ordre d’exécution des éléments pour pouvoir utiliser PurgeCSS. En effet Hugo doit tous d’abord générer l’ensemble des statistiques d’utilisation (et donc par extension toutes les pages HTML) avant de pouvoir exécuter la tâche de PurgeCSS. Ceci peut être réalisé via les Pipe PostProcess ou PostCSS de Hugo. Ceux-ci nous permettent de déporter la génération d’un contenu à la fin du processus de compilation.

Le composant PostCSS permettant automatiquement d’exécuter des commandes postcss (comme PurgeCSS), il sera parfaitement adapté à notre besoin. Nous allons donc commencer par installer la commande postcss.

# npm install -g postcss-cli
+ postcss-cli@7.1.0
added 152 packages from 157 contributors in 13.205s

Vous avez maintenant accès à la commande postcss dans votre shell. Nous allons créer la configuration de cette config pour lui expliquer ce qu’elle doit faire.

Créer un fichier postcss.config.js à la racine de votre projet avec ce contenu

module.exports = {
  plugins: [
  ]
}

Nous pouvons maintenant configurer le layout de notre site web pour activer le traitement de nos CSS. Dans votre fichier de template (themes/blog/layoutS/partials/headers.html dans mon cas) nous allons remplacer nos balises d’inclusion CSS. Par exemple avant modification j’avais cette ligne

<link rel="stylesheet" type="text/css" media="screen" href="{{ .Site.BaseURL }}css/normalize.css" />

Il convient de remplacer cette ligne comme ceci

{{ $css := resources.Get "css/main.css" }}
{{ $css = $css | resources.PostCSS | minify | resources.PostProcess }}
<link rel="stylesheet" type="text/css" media="screen" href="{{ $css.RelPermalink }}" />

Nous demandons ici à Hugo de récupérer le fichier d’asset css/main.css, de le faire passer dans l’outil PostCSS pour sauvegarder le résultat, le minifier et enfin d’afficher le lien qui a été généré pour ce fichier.

Note: Que faire si j’ai l’erreur error calling PostCSS: interface conversion: interface is nil ?

Il est possible que vous rencontriez cette erreur à la sauvegarde du code précédent. En effet la commande resources.Get qui récupère le fichier à traiter effectue des recherches dans les dossiers assets/ et non pas dans les dossiers static où devait se trouver vos fichiers statiques avant modification.

Il convient donc de créer dossier assets/ dans votre projet (à la base ou dans le thème), y déplacer vos fichiers CSS à l’intérieur et redémarrer hugo. Voici les commandes effectuées dans mon cas.

$ mkdir themes/blog/assets
$ mv themes/blog/static/css themes/blog/assets

Supprimer les ressource inutiles avec PurgeCSS

Alors on a bien avancé mais techniquement parlant actuellement nos pages sont exactement les mêmes qu’avant. Nous allons maintenant supprimer réellement nos ressources CSS inutiles en ajoutant le plugin purgecss dans notre configuration.

Modifier votre fichier postcss.config.js comme ceci

const purgecss = require('@fullhuman/postcss-purgecss')({
    content: [ './hugo_stats.json' ],
    defaultExtractor: (content) => {
        let els = JSON.parse(content).htmlElements;
        return els.tags.concat(els.classes, els.ids);
    }
});

module.exports = {
    plugins: [
        require('autoprefixer'),
        purgecss
    ]
};

Ici nous configurons postcss pour lui demander d’exécuter deux plugins PostCSS:

  • (autoprefixer)[https://github.com/postcss/autoprefixer]
  • (purgecss)[https://github.com/FullHuman/purgecss]

Vous devez constater que vous rencontrez une erreur, car ces deux modules npm ne sont pas encore installé. Nous pouvons ajouter ceci dans notre fichier package.json

$ npm init # Si vous n'avez pas de package.json
$ npm install autoprefixer
$ npm install fullhuman/postcss-purgecss

Et voilà! Les directives CSS superflues pour votre site ont été supprimés.

Aller plus loin

Activer uniquement pour la production

Le temps de génération des CSS peut être un peu long avec de gros frameworks. Si vous souhaitez générer les fichiers CSS purgés uniquement pour la production vous pouvez modifier vos templates comme ceci

  {{ $css := resources.Get "css/normalize.css" }}
  {{ if hugo.IsProduction }}
  {{ $css = $css | resources.PostCSS | fingerprint | resources.PostProcess }}
  {{ end }}
  <link rel="stylesheet" type="text/css" media="screen" href="{{ $css.Permalink }}" />

Et votre fichier postcss.config.js comme ceci

const purgecss = require('@fullhuman/postcss-purgecss')({
    content: [ './hugo_stats.json' ],
    whitelist: [
      'img'
    ],
    defaultExtractor: (content) => {
        let els = JSON.parse(content).htmlElements;
        return els.tags.concat(els.classes, els.ids);
    }
});

module.exports = {
    plugins: [
      require('autoprefixer'),
      ...(process.env.HUGO_ENVIRONMENT === 'production' ? [ purgecss ] : [])
    ]
};

Erreur sur les images

Comme PurgeCSS fonctionne sur un principe de liste blanche il est possible que si hugo ne détecte pas bien un tag HTML dans votre code il ne l’ajoute pas dans le fichier hugo_stats.json et ainsi que celui-ci soit supprimé par PurgeCSS. C’est ce qui m’est arrivé avec la balise HTML img.

Si vous souhaitez ajouter explicitement un tag, une classe ou un ID à la liste de blanche de PurgeCSS il est possible de le faire comme ceci.

const purgecss = require('@fullhuman/postcss-purgecss')({
    content: [ './hugo_stats.json' ],
    whitelist: [
      'img'
    ],
    defaultExtractor: (content) => {
        let els = JSON.parse(content).htmlElements;
        return els.tags.concat(els.classes, els.ids);
    }
});
...

Résultat et conclusion

Au final pour une page très simple j’avais 2 fichiers CSS pour un poids initial et total de 9.1Ko. Après purge (sans minification) je tombe à 6.1Ko. Enfin si j’active la minification j’arrive à 4.5Ko.

J’arrive donc à diviser la taille de mes ressources CSS par deux sur une page déjà très légère alors imaginer sur des pages plus complexe faisant intervenir de nombreux fichiers CSS.


  1. Comparatif des tailles des frameworks CSS ↩︎

  2. option writeStats ↩︎