Gestion des CSS inutilisés dans Sass pour améliorer les performances

Publié: 2022-03-10
Résumé rapide ↬ Connaissez-vous l'impact des CSS inutilisés sur les performances ? Spoiler : C'est beaucoup ! Dans cet article, nous allons explorer une solution orientée Sass pour traiter les CSS inutilisés, en évitant le besoin de dépendances Node.js compliquées impliquant des navigateurs sans tête et l'émulation DOM.

Dans le développement frontal moderne, les développeurs doivent viser à écrire un CSS évolutif et maintenable. Sinon, ils risquent de perdre le contrôle sur des détails tels que la spécificité de la cascade et du sélecteur à mesure que la base de code se développe et que davantage de développeurs contribuent.

Une façon d'y parvenir est d'utiliser des méthodologies telles que le CSS orienté objet (OOCSS), qui plutôt que d'organiser le CSS autour du contexte de la page, encourage la séparation de la structure (systèmes de grille, espacement, largeurs, etc.) de la décoration (polices, marque, couleurs, etc.).

Ainsi, les noms de classe CSS tels que :

  • .blog-right-column
  • .latest_topics_list
  • .job-vacancy-ad

Sont remplacés par des alternatives plus réutilisables, qui appliquent les mêmes styles CSS, mais ne sont liées à aucun contexte particulier :

  • .col-md-4
  • .list-group
  • .card

Cette approche est généralement mise en œuvre à l'aide d'un framework Sass tel que Bootstrap, Foundation, ou de plus en plus souvent, un framework sur mesure qui peut être façonné pour mieux s'adapter au projet.

Plus après saut! Continuez à lire ci-dessous ↓

Alors maintenant, nous utilisons des classes CSS sélectionnées à partir d'un cadre de modèles, de composants d'interface utilisateur et de classes utilitaires. L'exemple ci-dessous illustre un système de grille commun construit à l'aide de Bootstrap, qui s'empile verticalement, puis une fois le point d'arrêt md atteint, passe à une disposition à 3 colonnes.

 <div class="container"> <div class="row"> <div class="col-12 col-md-4">Column 1</div> <div class="col-12 col-md-4">Column 2</div> <div class="col-12 col-md-4">Column 3</div> </div> </div>

Les classes générées par programme telles que .col-12 et .col-md-4 sont utilisées ici pour créer ce modèle. Mais qu'en est-il de .col-1 à .col-11 , .col-lg-4 , .col-md-6 ou .col-sm-12 ? Ce sont tous des exemples de classes qui seront incluses dans la feuille de style CSS compilée, téléchargées et analysées par le navigateur, bien qu'elles ne soient pas utilisées.

Dans cet article, nous commencerons par explorer l'impact que les CSS inutilisés peuvent avoir sur les vitesses de chargement des pages. Nous aborderons ensuite une solution existante pour la supprimer des feuilles de style, en poursuivant avec ma propre solution orientée Sass.

Mesurer l'impact des classes CSS inutilisées

Alors que j'adore Sheffield United, les puissantes lames, le CSS de leur site Web est regroupé dans un seul fichier minifié de 568 Ko, qui atteint 105 Ko même lorsqu'il est compressé. Cela semble beaucoup.

Ceci est le site Web de Sheffield United, mon équipe de football locale (c'est le football pour vous dans les colonies). ( Grand aperçu )

Allons-nous voir à quel point ce CSS est réellement utilisé sur leur page d'accueil ? Une recherche rapide sur Google révèle de nombreux outils en ligne, mais je préfère utiliser l'outil de couverture de Chrome, qui peut être exécuté directement à partir de DevTools de Chrome. Donnons-lui un tourbillon.

Le moyen le plus rapide d'accéder à l'outil de couverture dans les outils de développement consiste à utiliser le raccourci clavier Ctrl+Maj+P ou Commande+Maj+P (Mac) pour ouvrir le menu de commandes. Dans celui-ci, tapez coverage et sélectionnez l'option "Afficher la couverture". ( Grand aperçu )

Les résultats montrent que seuls 30 Ko de CSS de la feuille de style de 568 Ko sont utilisés par la page d'accueil, les 538 Ko restants étant liés aux styles requis pour le reste du site Web. Cela signifie qu'un énorme 94,8% du CSS est inutilisé.

Vous pouvez voir des horaires comme ceux-ci pour n'importe quel actif dans Chrome dans les outils de développement via Réseau -> Cliquez sur votre actif -> onglet Timing. ( Grand aperçu )

Le CSS fait partie du chemin de rendu critique d'une page Web, qui implique toutes les différentes étapes qu'un navigateur doit effectuer avant de pouvoir commencer le rendu de la page. Cela fait de CSS un atout bloquant le rendu.

Donc, dans cet esprit, lors du chargement du site Web de Sheffield United à l'aide d'une bonne connexion 3G, il faut 1,15 s avant que le CSS ne soit téléchargé et que le rendu de la page puisse commencer. C'est un problème.

Google l'a également reconnu. Lors de l'exécution d'un audit Lighthouse, en ligne ou via votre navigateur, toutes les économies potentielles de temps de chargement et de taille de fichier qui pourraient être réalisées en supprimant les CSS inutilisés sont mises en évidence.

Dans Chrome (et Chromium Edge), vous pouvez corriger les audits Google Lighthouse en cliquant sur l'onglet Audit dans les outils de développement. ( Grand aperçu )

Solutions existantes

L'objectif est de déterminer quelles classes CSS ne sont pas nécessaires et de les supprimer de la feuille de style. Il existe des solutions existantes qui tentent d'automatiser ce processus. Ils peuvent généralement être utilisés via un script de construction Node.js ou via des exécuteurs de tâches tels que Gulp. Ceux-ci inclus:

  • UNCSS
  • PurifyCSS
  • PurgeCSS

Ceux-ci fonctionnent généralement de la même manière :

  1. Sur bulld, le site est accessible via un navigateur sans tête (Ex : puppeteer) ou une émulation DOM (Ex : jsdom).
  2. Sur la base des éléments HTML de la page, tout CSS inutilisé est identifié.
  3. Ceci est supprimé de la feuille de style, ne laissant que ce qui est nécessaire.

Bien que ces outils automatisés soient parfaitement valides et que j'en ai utilisé beaucoup dans un certain nombre de projets commerciaux avec succès, j'ai rencontré quelques inconvénients en cours de route qui méritent d'être partagés :

  • Si les noms de classe contiennent des caractères spéciaux tels que '@' ou '/', ceux-ci peuvent ne pas être reconnus sans écrire du code personnalisé. J'utilise BEM-IT de Harry Roberts, qui consiste à structurer les noms de classe avec des suffixes réactifs tels que: u-width-6/12@lg , donc j'ai déjà rencontré ce problème.
  • Si le site Web utilise un déploiement automatisé, cela peut ralentir le processus de construction, surtout si vous avez beaucoup de pages et beaucoup de CSS.
  • Les connaissances sur ces outils doivent être partagées au sein de l'équipe, sinon il peut y avoir confusion et frustration lorsque le CSS est mystérieusement absent des feuilles de style de production.
  • Si votre site Web comporte de nombreux scripts tiers en cours d'exécution, parfois lorsqu'ils sont ouverts dans un navigateur sans tête, ceux-ci ne fonctionnent pas bien et peuvent provoquer des erreurs avec le processus de filtrage. Donc, généralement, vous devez écrire un code personnalisé pour exclure tout script tiers lorsqu'un navigateur sans tête est détecté, ce qui, selon votre configuration, peut être délicat.
  • Généralement, ces types d'outils sont compliqués et introduisent de nombreuses dépendances supplémentaires dans le processus de construction. Comme c'est le cas avec toutes les dépendances tierces, cela signifie s'appuyer sur le code de quelqu'un d'autre.

Avec ces points à l'esprit, je me suis posé une question:

En utilisant uniquement Sass, est-il possible de mieux gérer le Sass que nous compilons afin que tout CSS inutilisé puisse être exclu, sans avoir recours à la simple suppression grossière des classes source dans le Sass ?

Alerte spoiler : la réponse est oui. Voici ce que j'ai trouvé.

Solution orientée Sass

La solution doit fournir un moyen rapide et facile de sélectionner ce que Sass doit être compilé, tout en étant suffisamment simple pour ne pas ajouter plus de complexité au processus de développement ou empêcher les développeurs de tirer parti de choses comme le CSS généré par programme. Des classes.

Pour commencer, il existe un référentiel avec des scripts de construction et quelques exemples de styles que vous pouvez cloner à partir d'ici.

Conseil : Si vous êtes bloqué, vous pouvez toujours faire une référence croisée avec la version terminée sur la branche master.

cd dans le dépôt, exécutez npm install puis npm run build pour compiler n'importe quel Sass en CSS selon les besoins. Cela devrait créer un fichier CSS de 55 Ko dans le répertoire dist.

Si vous ouvrez ensuite /dist/index.html dans votre navigateur Web, vous devriez voir un composant assez standard, qui se développe au clic pour révéler du contenu. Vous pouvez également le voir ici, où les conditions réelles du réseau seront appliquées, afin que vous puissiez exécuter vos propres tests.

Nous utiliserons ce composant d'interface utilisateur d'extension comme sujet de test lors du développement de la solution orientée Sass pour la gestion des CSS inutilisés. ( Grand aperçu )

Filtrage au niveau des partiels

Dans une configuration SCSS typique, vous aurez probablement un seul fichier manifeste (par exemple : main.scss dans le référentiel), ou un par page (par exemple : index.scss , products.scss , contact.scss ) où les partiels du framework sont importés. Conformément aux principes OOCSS, ces importations peuvent ressembler à ceci :

Exemple 1

 /* Undecorated design patterns */ @import 'objects/box'; @import 'objects/container'; @import 'objects/layout'; /* UI components */ @import 'components/button'; @import 'components/expander'; @import 'components/typography'; /* Highly specific helper classes */ @import 'utilities/alignments'; @import 'utilities/widths';

Si l'un de ces partiels n'est pas utilisé, la manière naturelle de filtrer ce CSS inutilisé serait simplement de désactiver l'importation, ce qui l'empêcherait d'être compilé.

Par exemple, si vous n'utilisez que le composant d'extension, le manifeste ressemblera généralement à ce qui suit :

Exemple 2

 /* Undecorated design patterns */ // @import 'objects/box'; // @import 'objects/container'; // @import 'objects/layout'; /* UI components */ // @import 'components/button'; @import 'components/expander'; // @import 'components/typography'; /* Highly specific helper classes */ // @import 'utilities/alignments'; // @import 'utilities/widths';

Cependant, conformément à OOCSS, nous séparons la décoration de la structure pour permettre une réutilisation maximale, il est donc possible que l'expandeur nécessite le CSS d'autres objets, composants ou classes utilitaires pour s'afficher correctement. À moins que le développeur ne soit conscient de ces relations en inspectant le code HTML, il se peut qu'il ne sache pas importer ces partiels, de sorte que toutes les classes requises ne seront pas compilées.

Dans le référentiel, si vous regardez le code HTML de l'expandeur dans dist/index.html , cela semble être le cas. Il utilise les styles des objets de boîte et de mise en page, le composant de typographie et les utilitaires de largeur et d'alignement.

dist/index.html

 <div class="c-expander"> <div class="o-box o-box--spacing-small c-expander__trigger c-expander__header" tabindex="0"> <div class="o-layout o-layout--fit u-flex-middle"> <div class="o-layout__item u-width-grow"> <h2 class="c-type-echo">Toggle Expander</h2> </div> <div class="o-layout__item u-width-shrink"> <div class="c-expander__header-icon"></div> </div> </div> </div> <div class="c-expander__content"> <div class="o-box o-box--spacing-small"> Lorum ipsum <p class="u-align-center"> <button class="c-expander__trigger c-button">Close</button> </p> </div> </div> </div>

Réglons ce problème qui attend de se produire en officialisant ces relations dans le Sass lui-même, donc une fois qu'un composant est importé, toutes les dépendances seront également importées automatiquement. De cette façon, le développeur n'a plus la surcharge supplémentaire d'avoir à auditer le HTML pour savoir ce qu'il doit importer d'autre.

Carte des importations programmatiques

Pour que ce système de dépendance fonctionne, plutôt que de simplement commenter les instructions @import dans le fichier manifeste, la logique Sass devra dicter si les partiels seront compilés ou non.

Dans src/scss/settings , créez un nouveau partiel appelé _imports.scss , @import -le dans settings/_core.scss , puis créez la carte SCSS suivante :

src/scss/settings/_core.scss

 @import 'breakpoints'; @import 'spacing'; @import 'imports';

src/scss/settings/_imports.scss

 $imports: ( object: ( 'box', 'container', 'layout' ), component: ( 'button', 'expander', 'typography' ), utility: ( 'alignments', 'widths' ) );

Cette carte aura le même rôle que le manifeste d'importation dans l'exemple 1.

Exemple 4

 $imports: ( object: ( //'box', //'container', //'layout' ), component: ( //'button', 'expander', //'typography' ), utility: ( //'alignments', //'widths' ) );

Il devrait se comporter comme un ensemble standard de @imports , en ce sens que si certains partiels sont commentés (comme ci-dessus), alors ce code ne devrait pas être compilé lors de la construction.

Mais comme nous souhaitons importer automatiquement les dépendances, nous devrions également pouvoir ignorer cette carte dans les bonnes circonstances.

Mixage de rendu

Commençons à ajouter un peu de logique Sass. Créez _render.scss dans src/scss/tools , puis ajoutez son @import à tools/_core.scss .

Dans le fichier, créez un mixin vide appelé render() .

src/scss/tools/_render.scss

 @mixin render() { }

Dans le mixin, nous devons écrire Sass qui fait ce qui suit :

  • rendre()
    "Hé, $imports , beau temps n'est-ce pas ? Dites, avez-vous l'objet conteneur dans votre carte ?"
  • $ importations
    false
  • rendre()
    "C'est dommage, on dirait que ça ne sera pas compilé alors. Qu'en est-il du composant bouton ? »
  • $ importations
    true
  • rendre()
    "Agréable! C'est le bouton en cours de compilation alors. Dites bonjour à la femme pour moi.

Dans Sass, cela se traduit par ce qui suit :

src/scss/tools/_render.scss

 @mixin render($name, $layer) { @if(index(map-get($imports, $layer), $name)) { @content; } }

Fondamentalement, vérifiez si le partiel est inclus dans la variable $imports , et si c'est le cas, rendez-le en utilisant la directive @content de Sass, qui nous permet de passer un bloc de contenu dans le mixin.

Nous l'utiliserions ainsi :

Exemple 5

 @include render('button', 'component') { .c-button { // styles et al } // any other class declarations }

Avant d'utiliser ce mixin, il y a une petite amélioration que nous pouvons lui apporter. Le nom de la couche (objet, composant, utilitaire, etc.) est quelque chose que nous pouvons prédire en toute sécurité, nous avons donc la possibilité de rationaliser un peu les choses.

Avant la déclaration de mixin de rendu, créez une variable appelée $layer et supprimez la variable portant le même nom des paramètres mixins. Ainsi:

src/scss/tools/_render.scss

 $layer: null !default; @mixin render($name) { @if(index(map-get($imports, $layer), $name)) { @content; } }

Maintenant, dans les partiels _core.scss où se trouvent les objets, les composants et les utilitaires @imports , redéclarez ces variables aux valeurs suivantes ; représentant le type de classes CSS importées.

src/scss/objects/_core.scss

 $layer: 'object'; @import 'box'; @import 'container'; @import 'layout';

src/scss/components/_core.scss

 $layer: 'component'; @import 'button'; @import 'expander'; @import 'typography';

src/scss/utilities/_core.scss

 $layer: 'utility'; @import 'alignments'; @import 'widths';

De cette façon, lorsque nous utilisons le mixin render() , tout ce que nous avons à faire est de déclarer le nom partiel.

Enveloppez le mixin render() autour de chaque déclaration d'objet, de composant et de classe utilitaire, comme indiqué ci-dessous. Cela vous donnera une utilisation de mixin de rendu par partiel.

Par exemple:

src/scss/objects/_layout.scss

 @include render('button') { .c-button { // styles et al } // any other class declarations }

src/scss/components/_button.scss

 @include render('button') { .c-button { // styles et al } // any other class declarations }

Remarque : Pour utilities/_widths.scss , envelopper la fonction render() autour de l'intégralité du partiel entraînera une erreur lors de la compilation, comme dans Sass, vous ne pouvez pas imbriquer les déclarations de mixin dans les appels de mixin. Au lieu de cela, enveloppez simplement le mixin render() autour des appels create-widths() , comme ci-dessous :

 @include render('widths') { // GENERATE STANDARD WIDTHS //--------------------------------------------------------------------- // Example: .u-width-1/3 @include create-widths($utility-widths-sets); // GENERATE RESPONSIVE WIDTHS //--------------------------------------------------------------------- // Create responsive variants using settings.breakpoints // Changes width when breakpoint is hit // Example: .u-width-1/3@md @each $bp-name, $bp-value in $mq-breakpoints { @include mq(#{$bp-name}) { @include create-widths($utility-widths-sets, \@, #{$bp-name}); } } // End render }

Avec cela en place, lors de la construction, seuls les partiels référencés dans $imports seront compilés.

Mélangez et associez les composants commentés dans $imports et exécutez npm run build dans le terminal pour l'essayer.

Carte des dépendances

Maintenant que nous importons par programmation des partiels, nous pouvons commencer à implémenter la logique de dépendance.

Dans src/scss/settings , créez un nouveau partiel appelé _dependencies.scss , @import dans settings/_core.scss , mais assurez-vous qu'il se trouve après _imports.scss . Ensuite, créez-y la carte SCSS suivante :

src/scss/settings/_dependencies.scss

 $dependencies: ( expander: ( object: ( 'box', 'layout' ), component: ( 'button', 'typography' ), utility: ( 'alignments', 'widths' ) ) );

Ici, nous déclarons des dépendances pour le composant d'expansion car il nécessite des styles d'autres partiels pour s'afficher correctement, comme on le voit dans dist/index.html.

En utilisant cette liste, nous pouvons écrire une logique qui signifierait que ces dépendances seraient toujours compilées avec leurs composants dépendants, quel que soit l'état de la variable $imports .

Sous $dependencies , créez un mixin appelé dependency-setup() . Ici, nous allons faire les actions suivantes :

1. Parcourez la carte des dépendances.

 @mixin dependency-setup() { @each $componentKey, $componentValue in $dependencies { } }

2. Si le composant se trouve dans $imports , parcourez sa liste de dépendances.

 @mixin dependency-setup() { $components: map-get($imports, component); @each $componentKey, $componentValue in $dependencies { @if(index($components, $componentKey)) { @each $layerKey, $layerValue in $componentValue { } } } }

3. Si la dépendance n'est pas dans $imports , ajoutez-la.

 @mixin dependency-setup() { $components: map-get($imports, component); @each $componentKey, $componentValue in $dependencies { @if(index($components, $componentKey)) { @each $layerKey, $layerValue in $componentValue { @each $partKey, $partValue in $layerValue { @if not index(map-get($imports, $layerKey), $partKey) { $imports: map-merge($imports, ( $layerKey: append(map-get($imports, $layerKey), '#{$partKey}') )) !global; } } } } } }

Inclure le drapeau !global indique à Sass de rechercher la variable $imports dans la portée globale, plutôt que dans la portée locale du mixin.

4. Ensuite, il suffit d'appeler le mixin.

 @mixin dependency-setup() { ... } @include dependency-setup();

Nous avons donc maintenant un système d'importation partielle amélioré, où si un composant est importé, un développeur n'a pas à importer manuellement chacun de ses divers partiels de dépendance.

Configurez la variable $imports afin que seul le composant d'extension soit importé, puis exécutez npm run build . Vous devriez voir dans le CSS compilé les classes d'extension avec toutes ses dépendances.

Cependant, cela n'apporte rien de nouveau en termes de filtrage des CSS inutilisés, car la même quantité de Sass est toujours importée, programmatique ou non. Améliorons cela.

Amélioration de l'importation des dépendances

Un composant peut ne nécessiter qu'une seule classe d'une dépendance, donc continuer et importer toutes les classes de cette dépendance conduit simplement au même ballonnement inutile que nous essayons d'éviter.

Nous pouvons affiner le système pour permettre un filtrage plus granulaire classe par classe, afin de nous assurer que les composants sont compilés uniquement avec les classes de dépendance dont ils ont besoin.

Avec la plupart des modèles de conception, décorés ou non, il existe un nombre minimum de classes qui doivent être présentes dans la feuille de style pour que le modèle s'affiche correctement.

Pour les noms de classe utilisant une convention de dénomination établie telle que BEM, les classes nommées « Bloc » et « Élément » sont généralement requises au minimum, les « Modificateurs » étant généralement facultatifs.

Remarque : les classes utilitaires ne suivraient généralement pas la route BEM, car elles sont isolées par nature en raison de leur focalisation étroite.

Par exemple, jetez un œil à cet objet multimédia, qui est probablement l'exemple le plus connu de CSS orienté objet :

 <div class="o-media o-media--spacing-small"> <div class="o-media__image"> <img src="url" alt="Image"> </div> <div class="o-media__text"> Oh! </div> </div>

Si un composant a cet ensemble comme dépendance, il est logique de toujours compiler .o-media , .o-media__image et .o-media__text , car c'est la quantité minimale de CSS requise pour faire fonctionner le modèle. Cependant, avec .o-media--spacing-small étant un modificateur facultatif, il ne doit être compilé que si nous le disons explicitement, car son utilisation peut ne pas être cohérente dans toutes les instances d'objet multimédia.

Nous allons modifier la structure de la carte $dependencies pour nous permettre d'importer ces classes facultatives, tout en incluant un moyen d'importer uniquement le bloc et l'élément au cas où aucun modificateur n'est requis.

Pour commencer, vérifiez le code HTML de l'expandeur dans dist/index.html et notez toutes les classes de dépendance utilisées. Enregistrez-les dans la carte $dependencies , comme ci-dessous :

src/scss/settings/_dependencies.scss

 $dependencies: ( expander: ( object: ( box: ( 'o-box--spacing-small' ), layout: ( 'o-layout--fit' ) ), component: ( button: true, typography: ( 'c-type-echo', ) ), utility: ( alignments: ( 'u-flex-middle', 'u-align-center' ), widths: ( 'u-width-grow', 'u-width-shrink' ) ) ) );

Lorsqu'une valeur est définie sur true, nous traduirons cela par "Compiler uniquement les classes de niveau bloc et élément, pas de modificateurs!".

L'étape suivante consiste à créer une variable de liste blanche pour stocker ces classes et toutes les autres classes (sans dépendance) que nous souhaitons importer manuellement. Dans /src/scss/settings/imports.scss , après $imports , créez une nouvelle liste Sass appelée $global-filter .

src/scss/settings/_imports.scss

 $global-filter: ();

Le principe de base derrière $global-filter est que toutes les classes stockées ici seront compilées lors de la construction tant que le partiel auquel elles appartiennent est importé via $imports .

Ces noms de classe peuvent être ajoutés par programme s'il s'agit d'une dépendance de composant, ou peuvent être ajoutés manuellement lorsque la variable est déclarée, comme dans l'exemple ci-dessous :

Exemple de filtre global

 $global-filter: ( 'o-box--spacing-regular@md', 'u-align-center', 'u-width-6/12@lg' );

Ensuite, nous devons ajouter un peu plus de logique au mixin @dependency-setup , afin que toutes les classes référencées dans $dependencies soient automatiquement ajoutées à notre liste blanche $global-filter .

Sous ce bloc :

src/scss/settings/_dependencies.scss

 @if not index(map-get($imports, $layerKey), $partKey) { }

... ajoutez l'extrait suivant.

src/scss/settings/_dependencies.scss

 @each $class in $partValue { $global-filter: append($global-filter, '#{$class}', 'comma') !global; }

Cela parcourt toutes les classes de dépendance et les ajoute à la liste blanche $global-filter .

À ce stade, si vous ajoutez une instruction @debug sous le mixin dependency-setup() pour imprimer le contenu de $global-filter dans le terminal :

 @debug $global-filter;

... vous devriez voir quelque chose comme ceci sur la construction :

 DEBUG: "o-box--spacing-small", "o-layout--fit", "c-box--rounded", "true", "true", "u-flex-middle", "u-align-center", "u-width-grow", "u-width-shrink"

Maintenant que nous avons une liste blanche de classes, nous devons l'appliquer à tous les différents partiels d'objets, de composants et d'utilitaires.

Créez un nouveau partiel appelé _filter.scss dans src/scss/tools et ajoutez un @import au fichier _core.scss de la couche d'outils.

Dans ce nouveau partiel, nous allons créer un mixin appelé filter() . Nous l'utiliserons pour appliquer la logique, ce qui signifie que les classes ne seront compilées que si elles sont incluses dans la variable $global-filter .

En partant simplement, créez un mixin qui accepte un seul paramètre - la $class contrôlée par le filtre. Ensuite, si la $class est incluse dans la liste blanche $global-filter , autorisez sa compilation.

src/scss/tools/_filter.scss

 @mixin filter($class) { @if(index($global-filter, $class)) { @content; } }

Dans un partiel, nous enroulerions le mixin autour d'une classe optionnelle, comme ceci :

 @include filter('o-myobject--modifier') { .o-myobject--modifier { color: yellow; } }

Cela signifie que la classe .o-myobject--modifier ne serait compilée que si elle était incluse dans $global-filter , qui peut être définie directement ou indirectement via ce qui est défini dans $dependencies .

Parcourez le référentiel et appliquez le mixin filter() à toutes les classes de modificateurs facultatives sur les couches d'objets et de composants. Lors de la gestion du composant typographie ou de la couche utilitaires, comme chaque classe est indépendante de la suivante, il serait logique de les rendre toutes facultatives, afin que nous puissions ensuite simplement activer les classes selon nos besoins.

Voici quelques exemples :

src/scss/objects/_layout.scss

 @include filter('o-layout__item--fit-height') { .o-layout__item--fit-height { align-self: stretch; } }

src/scss/utilities/_alignments.scss

 // Changes alignment when breakpoint is hit // Example: .u-align-left@md @each $bp-name, $bp-value in $mq-breakpoints { @include mq(#{$bp-name}) { @include filter('u-align-left@#{$bp-name}') { .u-align-left\@#{$bp-name} { text-align: left !important; } } @include filter('u-align-center@#{$bp-name}') { .u-align-center\@#{$bp-name} { text-align: center !important; } } @include filter('u-align-right@#{$bp-name}') { .u-align-right\@#{$bp-name} { text-align: right !important; } } } }

Remarque : lors de l'ajout des noms de classe de suffixe responsive au mixin filter() , vous n'avez pas besoin d'échapper le symbole '@' avec un '\'.

Au cours de ce processus, lors de l'application du mixin filter() aux partiels, vous avez peut-être (ou non) remarqué certaines choses.

Cours groupés

Certaines classes de la base de code sont regroupées et partagent les mêmes styles, par exemple :

src/scss/objects/_box.scss

 .o-box--spacing-disable-left, .o-box--spacing-horizontal { padding-left: 0; }

Comme le filtre n'accepte qu'une seule classe, il ne tient pas compte de la possibilité qu'un bloc de déclaration de style puisse être pour plus d'une classe.

Pour tenir compte de cela, nous allons développer le mixin filter() afin qu'en plus d'une seule classe, il soit capable d'accepter une liste d'arguments Sass contenant de nombreuses classes. Ainsi:

src/scss/objects/_box.scss

 @include filter('o-box--spacing-disable-left', 'o-box--spacing-horizontal') { .o-box--spacing-disable-left, .o-box--spacing-horizontal { padding-left: 0; } }

Nous devons donc indiquer au mixin filter() que si l'une de ces classes se trouve dans $global-filter , vous êtes autorisé à compiler les classes.

Cela impliquera une logique supplémentaire pour vérifier le type de l'argument $class du mixin, répondant avec une boucle si un arglist est passé pour vérifier si chaque élément est dans la variable $global-filter .

src/scss/tools/_filter.scss

 @mixin filter($class...) { @if(type-of($class) == 'arglist') { @each $item in $class { @if(index($global-filter, $item)) { @content; } } } @else if(index($global-filter, $class)) { @content; } }

Ensuite, il suffit de revenir aux partiels suivants pour appliquer correctement le mixin filter() :

  • objects/_box.scss
  • objects/_layout.scss
  • utilities/_alignments.scss

À ce stade, revenez à $imports et activez uniquement le composant d'extension. Dans la feuille de style compilée, outre les styles des couches génériques et éléments, vous ne devriez voir que les éléments suivants :

  • Les classes de bloc et d'élément appartenant au composant d'expansion, mais pas à son modificateur.
  • Les classes de blocs et d'éléments appartenant aux dépendances de l'expandeur.
  • Toutes les classes de modificateurs appartenant aux dépendances de l'expandeur qui sont explicitement déclarées dans la variable $dependencies .

Théoriquement, si vous décidez d'inclure plus de classes dans la feuille de style compilée, comme le modificateur de composants d'extension, il suffit de l'ajouter à la variable $global-filter au point de déclaration, ou de l'ajouter à un autre point dans la base de code (tant qu'il est avant le point où le modificateur lui-même est déclaré).

Tout activer

Nous avons donc maintenant un système assez complet, qui vous permet d'importer des objets, des composants et des utilitaires jusqu'aux classes individuelles de ces partiels.

Pendant le développement, pour une raison quelconque, vous voudrez peut-être tout activer en une seule fois. Pour permettre cela, nous allons créer une nouvelle variable appelée $enable-all-classes , puis ajouter une logique supplémentaire, donc si cela est défini sur true, tout est compilé quel que soit l'état de $imports et $global-filter variables de $global-filter .

Tout d'abord, déclarez la variable dans notre fichier manifeste principal :

src/scss/main.scss

 $enable-all-classes: false; @import 'settings/core'; @import 'tools/core'; @import 'generic/core'; @import 'elements/core'; @import 'objects/core'; @import 'components/core'; @import 'utilities/core';

Ensuite, nous avons juste besoin d'apporter quelques modifications mineures à nos mixins filter() et render() pour ajouter une logique de remplacement lorsque la variable $enable-all-classes est définie sur true.

Tout d'abord, le mixin filter() . Avant toute vérification existante, nous ajouterons une instruction @if pour voir si $enable-all-classes est défini sur true, et si c'est le cas, rendons le @content , sans poser de questions.

src/scss/tools/_filter.scss

 @mixin filter($class...) { @if($enable-all-classes) { @content; } @else if(type-of($class) == 'arglist') { @each $item in $class { @if(index($global-filter, $item)) { @content; } } } @else if(index($global-filter, $class)) { @content; } }

Ensuite, dans le mixin render() , nous avons juste besoin de faire une vérification pour voir si la variable $enable-all-classes est véridique, et si c'est le cas, ignorez toute autre vérification.

src/scss/tools/_render.scss

 $layer: null !default; @mixin render($name) { @if($enable-all-classes or index(map-get($imports, $layer), $name)) { @content; } }

Alors maintenant, si vous deviez définir la variable $enable-all-classes sur true et reconstruire, chaque classe facultative serait compilée, ce qui vous ferait gagner un peu de temps dans le processus.

Comparaisons

Pour voir quel type de gains cette technique nous donne, effectuons quelques comparaisons et voyons quelles sont les différences de taille de fichier.

Pour nous assurer que la comparaison est juste, nous devons ajouter les objets box et container dans $imports , puis ajouter le modificateur o-box--spacing-regular de la boîte au $global-filter , comme ceci :

src/scss/settings/_imports.scss

 $imports: ( object: ( 'box', 'container' // 'layout' ), component: ( // 'button', 'expander' // 'typography' ), utility: ( // 'alignments', // 'widths' ) ); $global-filter: ( 'o-box--spacing-regular' );

Cela garantit que les styles des éléments parents de l'expandeur sont compilés comme ils le seraient s'il n'y avait pas de filtrage.

Feuilles de style originales vs filtrées

Comparons la feuille de style d'origine avec toutes les classes compilées, à la feuille de style filtrée où seul le CSS requis par le composant d'expansion a été compilé.

Standard
Feuille de style Taille (ko) Taille (gzip)
Original 54.6ko 6.98ko
Filtré 15.34kb (72% plus petit) 4.91kb (29% plus petit)
  • Original : https://webdevluke.github.io/handlingunusedcss/dist/index2.html
  • Filtré : https://webdevluke.github.io/handlingunusedcss/dist/index.html

Vous pensez peut-être que les économies en pourcentage de gzip signifient que cela n'en vaut pas la peine, car il n'y a pas beaucoup de différence entre les feuilles de style d'origine et filtrées.

Il convient de souligner que la compression gzip fonctionne mieux avec des fichiers plus volumineux et plus répétitifs. Étant donné que la feuille de style filtrée est la seule preuve de concept et ne contient que du CSS pour le composant d'extension, il n'y a pas autant à compresser que dans un projet réel.

Si nous devions augmenter chaque feuille de style d'un facteur de 10 à des tailles plus typiques de la taille du bundle CSS d'un site Web, la différence de taille de fichier gzip serait beaucoup plus impressionnante.

10x Taille
Feuille de style Taille (ko) Taille (gzip)
Originale (10x) 892.07ko 75.70kb
Filtré (10x) 209.45kb (77% plus petit) 19.47kb (74% plus petit)

Feuille de style filtrée vs UNCSS

Voici une comparaison entre la feuille de style filtrée et une feuille de style qui a été exécutée via l'outil UNCSS.

Filtré vs UNCSS
Feuille de style Taille (ko) Taille (gzip)
Filtré 15.34ko 4.91kb
UNCSS 12.89kb (16% plus petit) 4.25kb (13% plus petit)

L'outil UNCSS gagne ici légèrement, car il filtre les CSS dans les répertoires génériques et éléments.

Il est possible que sur un vrai site Web, avec une plus grande variété d'éléments HTML utilisés, la différence entre les 2 méthodes soit négligeable.

Emballer

Nous avons donc vu comment, en utilisant uniquement Sass, vous pouvez mieux contrôler les classes CSS compilées lors de la construction. Cela réduit la quantité de CSS inutilisés dans la feuille de style finale et accélère le chemin de rendu critique.

Au début de l'article, j'ai énuméré quelques inconvénients des solutions existantes telles que l'UNCSS. Il est juste de critiquer cette solution orientée Sass de la même manière, donc tous les faits sont sur la table avant de décider quelle approche est la meilleure pour vous :

Avantages

  • Aucune dépendance supplémentaire n'est requise, vous n'avez donc pas à vous fier au code de quelqu'un d'autre.
  • Moins de temps de construction requis que les alternatives basées sur Node.js, car vous n'avez pas besoin d'exécuter des navigateurs sans tête pour auditer votre code. Ceci est particulièrement utile avec l'intégration continue car vous risquez moins de voir une file d'attente de builds.
  • Résultats dans une taille de fichier similaire par rapport aux outils automatisés.
  • Par défaut, vous avez un contrôle total sur le code filtré, quelle que soit la manière dont ces classes CSS sont utilisées dans votre code. Avec les alternatives basées sur Node.js, vous devez souvent maintenir une liste blanche distincte afin que les classes CSS appartenant au HTML injecté dynamiquement ne soient pas filtrées.

Les inconvénients

  • La solution orientée Sass est nettement plus pratique, dans le sens où vous devez vous tenir au courant des variables $imports et $global-filter . Au-delà de la configuration initiale, les alternatives Node.js que nous avons examinées sont largement automatisées.
  • Si vous ajoutez des classes CSS à $global-filter et que vous les supprimez ensuite de votre HTML, vous devez vous rappeler de mettre à jour la variable, sinon vous compilerez des CSS dont vous n'avez pas besoin. Avec de grands projets sur lesquels travaillent plusieurs développeurs à la fois, cela peut ne pas être facile à gérer à moins que vous ne le planifiiez correctement.
  • Je ne recommanderais pas de verrouiller ce système sur une base de code CSS existante, car vous devriez passer un peu de temps à reconstituer les dépendances et à appliquer le mixin render() à BEAUCOUP de classes. C'est un système beaucoup plus facile à mettre en œuvre avec de nouvelles versions, où vous n'avez pas de code existant à gérer.

J'espère que vous avez trouvé cela aussi intéressant à lire que je l'ai trouvé intéressant à mettre en place. Si vous avez des suggestions, des idées pour améliorer cette approche, ou si vous souhaitez signaler un défaut fatal que j'ai complètement manqué, assurez-vous de publier dans les commentaires ci-dessous.