Chapitre 48 : Analyse du code mort avec PurgeCSS
Identification et suppression des sélecteurs inutilisés via l'analyse statique
Le concept de "Dead Code" en CSS
1. Quoi
Le code mort (ou dead code) en CSS désigne l'ensemble des règles, sélecteurs et déclarations qui sont présents dans vos fichiers de styles mais qui ne sont jamais appliqués à aucun élément du DOM dans l'application finale.
PurgeCSS est un outil d'analyse statique qui scanne vos fichiers de templates (HTML, JSX, Vue, Svelte, etc.) et vos fichiers de scripts (JS, TS) pour identifier tous les noms de classes, IDs et sélecteurs utilisés. Il compare ensuite cette liste avec vos fichiers CSS et supprime tout ce qui n'est pas explicitement mentionné.
Contrairement aux outils d'analyse dynamique (qui observent le navigateur en temps réel), PurgeCSS travaille au moment du build, ce qui permet de réduire drastiquement la taille du bundle CSS avant même que l'utilisateur ne télécharge la page.
2. Pourquoi
Dans un projet professionnel, le CSS a tendance à croître de manière unidirectionnelle : on ajoute des styles pour de nouvelles fonctionnalités, mais on oublie rarement de supprimer les styles des fonctionnalités retirées. Ce phénomène est accentué par l'utilisation de frameworks CSS utilitaires (comme Tailwind CSS ou Bootstrap) où des milliers de classes sont disponibles, mais dont seulement 5 % sont réellement utilisées sur un projet donné.
L'impact d'un CSS trop volumineux est multiple :
- Performance de téléchargement : Plus le fichier est gros, plus le temps de transfert est long (impact sur le LCP - Largest Contentful Paint).
- Parse Time : Le navigateur doit analyser tout le CSS avant de pouvoir rendre la page.
- Maintenance : Un fichier CSS pollué rend la recherche de styles et le refactoring beaucoup plus complexes.
3. Comment
A. Syntaxe de base
PurgeCSS peut être utilisé via une CLI, mais il est plus courant de l'intégrer dans un pipeline de build (PostCSS, Webpack, Vite). Voici une configuration minimale via PostCSS :
// postcss.config.js
const purgecss = require('@fullhuman/postcss-purgecss')
module.exports = {
plugins: [
purgecss({
// On indique à PurgeCSS où chercher les classes utilisées
content: ['./src/**/*.html', './src/**/*.js', './src/**/*.vue']
})
]
}
B. Cas concret : Intégration robuste pour la production
Dans un environnement réel, on ne veut pas purger le CSS en développement (car cela ralentirait le HMR - Hot Module Replacement et pourrait supprimer des classes ajoutées dynamiquement). On l'active uniquement pour le build de production.
Voici une implémentation typique utilisant PostCSS et une liste de sauvegarde (safelist) pour éviter de supprimer des classes injectées par des bibliothèques tierces.
// postcss.config.js
const purgecss = require('@fullhuman/postcss-purgecss');
module.exports = {
plugins: [
// On n'active PurgeCSS que si l'environnement est 'production'
process.env.NODE_ENV === 'production'
? purgecss({
content: [
'./index.html',
'./src/**/*.{vue,js,ts,jsx,tsx}',
],
// La safelist empêche la suppression de classes critiques
safelist: {
standard: [
'active',
'is-loading',
/^nav-item-/ // Utilisation d'une regex pour garder toutes les classes commençant par nav-item-
],
deep: [/modal-content$/], // Garde les sélecteurs imbriqués
greedy: [/hljs-/] // Garde tout ce qui ressemble à une classe Highlight.js
},
// On peut spécifier quels fichiers CSS doivent être analysés
css: ['./src/assets/main.css']
})
: null,
]
};
C. Limitations
L'analyse statique a une limite fondamentale : elle ne "comprend" pas le code, elle cherche des chaînes de caractères.
Si vous construisez vos noms de classes dynamiquement en JavaScript, PurgeCSS ne pourra pas les détecter.
Exemple problématique :
// ❌ MAUVAIS : PurgeCSS ne trouvera jamais "btn-red" ou "btn-blue"
const color = 'red';
const className = `btn-${color}`;
Exemple correct :
// ✅ BON : Les chaînes complètes sont présentes dans le code
const colorClass = color === 'red' ? 'btn-red' : 'btn-blue';
4. Zone de Danger
❌ Erreur commune : Confier aveuglément le nettoyage du CSS à PurgeCSS sans tester les états dynamiques (modales, menus déroulants, tooltips). Ces éléments sont souvent injectés via JS ou activés par des classes ajoutées au clic, et peuvent disparaître si elles ne sont pas dans la safelist.
✅ Bonne pratique :
- Utiliser une
safelistpour les classes d'état (.is-open,.is-active). - Effecter un test de non-régression visuelle après le build de production.
- Écrire des classes CSS complètes dans le code JS/TS plutôt que de les concaténer.
Flux de fonctionnement de PurgeCSS
Questions clés
1. Quelle est la différence entre l'analyse statique et l'analyse dynamique pour le CSS ?
Découvrir la réponse
L'analyse statique (PurgeCSS) scanne le code source (fichiers texte) sans exécuter l'application pour trouver des correspondances de chaînes de caractères. L'analyse dynamique (Chrome DevTools "Coverage") observe quels styles sont réellement appliqués au DOM pendant que l'application tourne dans le navigateur.
2. Pourquoi PurgeCSS peut-il "casser" le design d'une application en production ?
Découvrir la réponse
Cela arrive généralement lorsque des classes sont générées dynamiquement (ex: class="text-" + color). PurgeCSS ne voit pas la chaîne complète text-red dans le code source et considère donc que la règle .text-red dans le CSS est inutile et la supprime.
3. À quoi sert la safelist ?
Découvrir la réponse
La safelist permet de forcer PurgeCSS à conserver certains sélecteurs, même s'il ne les trouve pas dans les fichiers de templates. C'est indispensable pour les classes ajoutées par des scripts externes ou des animations déclenchées dynamiquement.
4. Est-il recommandé d'utiliser PurgeCSS en environnement de développement ?
Découvrir la réponse
Non. Cela ralentirait le cycle de build et créerait des bugs frustrants où un nouveau style ajouté au HTML ne s'afficherait pas car le moteur de purge ne l'aurait pas encore indexé ou aurait besoin d'un redémarrage.
5. Quel est l'impact direct de PurgeCSS sur les Core Web Vitals ?
Découvrir la réponse
Il impacte principalement le LCP (Largest Contentful Paint) et le FCP (First Contentful Paint). En réduisant la taille du CSS bloquant le rendu, le navigateur peut construire l'arbre de rendu (Render Tree) plus rapidement et afficher le contenu à l'utilisateur plus tôt.
Mise en pratique
Exercice 1 : Reproduction guidée
Vous avez un fichier styles.css contenant 10 classes, mais votre fichier index.html n'en utilise que 3. Configurez un objet de configuration PurgeCSS simple qui analyserait index.html et définiriez une safelist pour garder la classe .js-hidden même si elle n'est pas dans le HTML.
Fichiers de départ :
index.html — seules 3 classes sont utilisées statiquement :
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Exercice PurgeCSS</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="container">
<h1 class="title">Bonjour PurgeCSS</h1>
<button class="btn-primary" id="btn-toggle">Masquer le titre</button>
<!-- Note : .js-hidden est ajoutée dynamiquement par app.js — absente du HTML statique -->
</div>
<script src="app.js"></script>
</body>
</html>
app.js — c'est ici que .js-hidden est utilisée, mais PurgeCSS ne peut pas la détecter car elle est construite dynamiquement :
// app.js
const btn = document.getElementById('btn-toggle');
const title = document.querySelector('.title');
btn.addEventListener('click', function () {
// ⚠️ PurgeCSS voit la chaîne 'js-hidden' dans ce fichier
// et peut donc la conserver — MAIS seulement si 'app.js'
// est inclus dans le champ 'content' de la configuration.
// Sans safelist ET sans scanner app.js, cette classe sera purgée.
title.classList.toggle('js-hidden');
btn.textContent = title.classList.contains('js-hidden')
? 'Afficher le titre'
: 'Masquer le titre';
});
.js-hidden disparaît quand on ne scanne que index.html ?PurgeCSS cherche les chaînes de caractères dans les fichiers listés dans content. Si vous n'y incluez que index.html, il ne voit jamais 'js-hidden' dans app.js et supprime donc la classe. Il existe deux solutions :
- Ajouter
app.jsdanscontent: PurgeCSS trouvera la chaîne dans le fichier JS. - Utiliser la
safelist: Forcer la conservation de.js-hiddenmême si elle est introuvable dans les sources scannées.
L'exercice vous demande d'utiliser la safelist — mais dans un projet réel, il est préférable d'inclure les fichiers JS dans content.
styles.css — 10 classes dont 7 sont du "code mort" :
/* ✅ Utilisées dans index.html */
.container { max-width: 800px; margin: 0 auto; padding: 2rem; }
.title { font-size: 2rem; color: #333; }
.btn-primary { background: #007bff; color: white; padding: 0.5rem 1rem; border: none; border-radius: 4px; cursor: pointer; }
/* ✅ Utilisée par JS — doit être dans la safelist */
.js-hidden { display: none; }
/* ❌ Code mort — jamais référencées dans le HTML ou le JS */
.btn-secondary { background: #6c757d; color: white; padding: 0.5rem 1rem; }
.btn-danger { background: #dc3545; color: white; padding: 0.5rem 1rem; }
.text-red { color: #dc3545; }
.text-blue { color: #007bff; }
.card { border: 1px solid #ddd; border-radius: 8px; padding: 1rem; }
.badge { font-size: 0.75rem; padding: 2px 6px; border-radius: 12px; }
Guide pour lancer la purge :
Étape 1 — Installer PurgeCSS CLI
npm install --save-dev purgecss
Étape 2 — Lancer la purge en ligne de commande
npx purgecss --css styles.css --content index.html --output styles.purged.css
--css: le fichier CSS source à analyser--content: le fichier HTML (ou glob) à scanner pour trouver les classes utilisées--output: dossier ou fichier de sortie
Étape 3 — Vérifier le résultat
Ouvrez styles.purged.css : vous ne devriez plus y trouver .btn-secondary, .btn-danger, .text-red, .text-blue, .card ou .badge. En revanche, .js-hidden aura disparu aussi — c'est pourquoi l'exercice demande de l'ajouter à la safelist dans la configuration.
Étape 4 — Utiliser un fichier de configuration pour la safelist
Créez un fichier purgecss.config.js :
module.exports = {
content: ['index.html'],
css: ['styles.css'],
output: 'dist/',
// safelist à compléter dans l'exercice
};
Puis lancez :
npx purgecss --config purgecss.config.js
Découvrir la solution commentée
// Configuration simplifiée pour l'exercice 1
const purgecssConfig = {
content: ['index.html'],
safelist: ['js-hidden'] // On force la conservation de cette classe
};
Exercice 2 : Adaptation (Le piège du dynamisme)
Le code suivant provoque la suppression des couleurs de boutons en production. Modifiez le code JavaScript pour qu'il soit compatible avec l'analyse statique de PurgeCSS.
// Code problématique
function getButtonClass(type) {
return 'btn-' + type; // type peut être 'success', 'danger', 'warning'
}
Découvrir la solution commentée
// Solution : Utiliser des chaînes de caractères complètes
function getButtonClass(type) {
const classes = {
success: 'btn-success',
danger: 'btn-danger',
warning: 'btn-warning'
};
// On retourne la valeur complète, PurgeCSS trouvera
// les strings 'btn-success', etc. dans ce fichier JS.
return classes[type] || 'btn-default';
}
Exercice 3 : Conception (Pipeline de production)
Imaginez que vous travaillez sur un projet utilisant un framework CSS externe (comme Bootstrap) et un framework JS (comme React). Vous remarquez que les composants de la bibliothèque externe (ex: Modales) perdent leur style après le passage de PurgeCSS.
Proposez une stratégie de configuration complète pour résoudre ce problème sans désactiver PurgeCSS.
Découvrir la solution commentée
// Stratégie de configuration robuste
module.exports = {
plugins: [
process.env.NODE_ENV === 'production'
? purgecss({
content: [
'./src/**/*.{jsx,tsx,html}',
'./public/index.html'
],
safelist: {
// 1. On utilise des regex pour protéger tous les composants de la lib
// Exemple : Bootstrap utilise souvent 'modal-...'
standard: [
/^modal-/,
/^dropdown-/,
/^show$/
],
// 2. On protège les classes d'état globales
deep: [/^is-/, /^has-/],
// 3. On peut ajouter des classes spécifiques connues pour être injectées
greedy: [/bs-carousel-/]
}
})
: null
]
};
/*
Explication de la stratégie :
1. Analyse exhaustive : On scanne tous les fichiers JSX/TSX pour capter les classes React.
2. Safelisting par pattern : Au lieu de lister 50 classes de modales, on utilise
une regex /^modal-/ pour protéger tout le bloc fonctionnel de la bibliothèque.
3. Protection des états : Les classes comme .is-active sont souvent ajoutées
via JS et ne sont pas présentes dans le JSX initial.
*/