Gestione di CSS inutilizzati in Sass per migliorare le prestazioni

Pubblicato: 2022-03-10
Riepilogo rapido ↬ Conosci l'impatto che i CSS non utilizzati hanno sulle prestazioni? Spoiler: è molto! In questo articolo, esploreremo una soluzione orientata a Sass per gestire CSS inutilizzati, evitando la necessità di complicate dipendenze Node.js che coinvolgono browser headless ed emulazione DOM.

Nel moderno sviluppo front-end, gli sviluppatori dovrebbero mirare a scrivere CSS che sia scalabile e manutenibile. Altrimenti, rischiano di perdere il controllo su specifiche come la cascata e la specificità del selettore man mano che la base di codice cresce e più sviluppatori contribuiscono.

Un modo in cui ciò può essere ottenuto è attraverso l'uso di metodologie come Object-Oriented CSS (OOCSS), che invece di organizzare i CSS attorno al contesto della pagina, incoraggia la separazione della struttura (sistemi di griglia, spaziatura, larghezze, ecc.) dalla decorazione (caratteri, marca, colori, ecc.).

Quindi nomi di classi CSS come:

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

Sono sostituiti con alternative più riutilizzabili, che applicano gli stessi stili CSS, ma non sono legate a nessun contesto particolare:

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

Questo approccio è comunemente implementato con l'aiuto di un framework Sass come Bootstrap, Foundation o, sempre più spesso, un framework su misura che può essere modellato per adattarsi meglio al progetto.

Altro dopo il salto! Continua a leggere sotto ↓

Quindi ora stiamo usando classi CSS selezionate da un framework di pattern, componenti dell'interfaccia utente e classi di utilità. L'esempio seguente illustra un sistema a griglia comune creato utilizzando Bootstrap, che si impila verticalmente, quindi una volta raggiunto il punto di interruzione md, passa a un layout a 3 colonne.

 <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>

Le classi generate a livello di codice come .col-12 e .col-md-4 vengono utilizzate qui per creare questo modello. Ma che dire di .col-1 fino .col-11 , .col-lg-4 , .col-md-6 o .col-sm-12 ? Questi sono tutti esempi di classi che verranno incluse nel foglio di stile CSS compilato, scaricate e analizzate dal browser, nonostante non siano in uso.

In questo articolo, inizieremo esplorando l'impatto che i CSS non utilizzati possono avere sulla velocità di caricamento della pagina. Toccheremo quindi alcune soluzioni esistenti per rimuoverlo dai fogli di stile, proseguendo con la mia soluzione orientata a Sass.

Misurare l'impatto delle classi CSS inutilizzate

Anche se adoro Sheffield United, i potenti blade, il CSS del loro sito Web è raggruppato in un unico file minimizzato da 568 kb, che arriva a 105 kb anche se compresso con gzip. Sembra molto.

Questo è il sito web dello Sheffield United, la mia squadra di football locale (questo è il calcio per te molto nelle colonie). (Grande anteprima)

Vediamo quanto di questo CSS viene effettivamente utilizzato sulla loro home page? Una rapida ricerca su Google rivela molti strumenti online all'altezza del lavoro, ma preferisco utilizzare lo strumento di copertura in Chrome, che può essere eseguito direttamente da DevTools di Chrome. Diamogli un vortice.

Il modo più rapido per accedere allo strumento di copertura in Strumenti per sviluppatori consiste nell'utilizzare la scorciatoia da tastiera Ctrl+Maiusc+P o Comando+Maiusc+P (Mac) per aprire il menu dei comandi. In esso, digita coverage e seleziona l'opzione "Mostra copertura". (Grande anteprima)

I risultati mostrano che solo 30kb di CSS del foglio di stile da 568kb vengono utilizzati dalla homepage, con i restanti 538kb relativi agli stili richiesti per il resto del sito web. Ciò significa che un enorme 94,8% del CSS non è utilizzato.

Puoi vedere tempi come questi per qualsiasi risorsa in Chrome in Strumenti per sviluppatori tramite rete -> Fai clic sulla tua risorsa -> scheda Tempistica. (Grande anteprima)

CSS fa parte del percorso di rendering critico di una pagina Web, che comprende tutti i diversi passaggi che un browser deve completare prima di poter iniziare il rendering della pagina. Ciò rende i CSS una risorsa che blocca la visualizzazione.

Quindi, con questo in mente, quando si carica il sito Web di Sheffield United utilizzando una buona connessione 3G, sono necessari 1,15 secondi prima che il CSS venga scaricato e possa iniziare il rendering della pagina. Questo è un problema.

Google ha riconosciuto anche questo. Quando si esegue un audit di Lighthouse, online o tramite il browser, vengono evidenziati tutti i potenziali risparmi sui tempi di caricamento e sulle dimensioni dei file che potrebbero essere ottenuti rimuovendo i CSS inutilizzati.

In Chrome (e Chromium Edge), puoi correggere gli audit di Google Lighthouse facendo clic sulla scheda Audit negli strumenti per sviluppatori. (Grande anteprima)

Soluzioni esistenti

L'obiettivo è determinare quali classi CSS non sono richieste e rimuoverle dal foglio di stile. Sono disponibili soluzioni esistenti che tentano di automatizzare questo processo. In genere possono essere utilizzati tramite uno script di build Node.js o tramite task runner come Gulp. Questi includono:

  • UNCSS
  • PurifyCSS
  • Purge CSS

Questi generalmente funzionano in modo simile:

  1. Su bulld, si accede al sito web tramite un browser headless (es: burattinaio) o emulazione DOM (es: jsdom).
  2. Sulla base degli elementi HTML della pagina, viene identificato qualsiasi CSS non utilizzato.
  3. Questo viene rimosso dal foglio di stile, lasciando solo ciò che è necessario.

Sebbene questi strumenti automatizzati siano perfettamente validi e ne abbia utilizzati molti in numerosi progetti commerciali con successo, ho riscontrato alcuni inconvenienti lungo il percorso che vale la pena condividere:

  • Se i nomi delle classi contengono caratteri speciali come '@' o '/', questi potrebbero non essere riconosciuti senza scrivere del codice personalizzato. Uso BEM-IT di Harry Roberts, che prevede la strutturazione di nomi di classi con suffissi reattivi come: u-width-6/12@lg , quindi ho già riscontrato questo problema.
  • Se il sito Web utilizza la distribuzione automatizzata, può rallentare il processo di creazione, soprattutto se hai molte pagine e molti CSS.
  • La conoscenza di questi strumenti deve essere condivisa con il team, altrimenti potrebbe esserci confusione e frustrazione quando i CSS sono misteriosamente assenti nei fogli di stile di produzione.
  • Se il tuo sito Web ha molti script di terze parti in esecuzione, a volte quando vengono aperti in un browser headless, questi non funzionano bene e possono causare errori con il processo di filtraggio. Quindi in genere devi scrivere un codice personalizzato per escludere eventuali script di terze parti quando viene rilevato un browser senza testa, il che, a seconda della configurazione, potrebbe essere complicato.
  • In genere, questo tipo di strumenti sono complicati e introducono molte dipendenze extra nel processo di compilazione. Come nel caso di tutte le dipendenze di terze parti, questo significa fare affidamento sul codice di qualcun altro.

Con questi punti in mente, mi sono posto una domanda:

Usando solo Sass, è possibile gestire meglio il Sass che compiliamo in modo da escludere qualsiasi CSS inutilizzato, senza ricorrere semplicemente alla rozza cancellazione delle classi sorgente nel Sass?

Avviso spoiler: la risposta è sì. Ecco cosa mi è venuto in mente.

Soluzione orientata al Sass

La soluzione deve fornire un modo semplice e veloce per scegliere cosa compilare Sass, pur essendo abbastanza semplice da non aggiungere ulteriore complessità al processo di sviluppo o impedire agli sviluppatori di trarre vantaggio da cose come i CSS generati a livello di codice classi.

Per iniziare, c'è un repository con script di build e alcuni stili di esempio che puoi clonare da qui.

Suggerimento: se rimani bloccato, puoi sempre fare un riferimento incrociato con la versione completata sul ramo principale.

cd nel repository, eseguire npm install e quindi npm run build per compilare qualsiasi Sass in CSS come richiesto. Questo dovrebbe creare un file CSS da 55kb nella directory dist.

Se poi apri /dist/index.html nel tuo browser web, dovresti vedere un componente abbastanza standard, che al clic si espande per rivelare alcuni contenuti. Puoi anche visualizzarlo qui, dove verranno applicate le condizioni di rete reali, in modo da poter eseguire i tuoi test.

Useremo questo componente dell'interfaccia utente di espansione come soggetto di prova durante lo sviluppo della soluzione orientata a Sass per la gestione dei CSS inutilizzati. (Grande anteprima)

Filtraggio a livello parziale

In una tipica configurazione SCSS, probabilmente avrai un singolo file manifest (ad esempio: main.scss nel repository) o uno per pagina (ad esempio: index.scss , products.scss , contact.scss ) dove i parziali del framework sono importati. Seguendo i principi OOCSS, tali importazioni potrebbero assomigliare a questo:

Esempio 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';

Se qualcuno di questi parziali non è in uso, il modo naturale per filtrare questo CSS inutilizzato sarebbe semplicemente disabilitare l'importazione, il che ne impedirebbe la compilazione.

Ad esempio, se si utilizza solo il componente expander, il manifest sarebbe in genere simile al seguente:

Esempio 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';

Tuttavia, come per OOCSS, stiamo separando la decorazione dalla struttura per consentire la massima riutilizzabilità, quindi è possibile che l'espansore possa richiedere CSS da altri oggetti, componenti o classi di utilità per il rendering corretto. A meno che lo sviluppatore non sia a conoscenza di queste relazioni esaminando l'HTML, potrebbe non sapere come importare questi parziali, quindi non tutte le classi richieste verrebbero compilate.

Nel repository, se guardi l'HTML dell'expander in dist/index.html , sembra che sia così. Utilizza gli stili della casella e degli oggetti layout, il componente tipografia e le utilità di larghezza e allineamento.

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>

Affrontiamo questo problema in attesa che si verifichi ufficializzando queste relazioni all'interno del Sass stesso, quindi una volta importato un componente, verranno importate automaticamente anche le eventuali dipendenze. In questo modo, lo sviluppatore non ha più il sovraccarico di dover controllare l'HTML per sapere cos'altro deve importare.

Mappa delle importazioni programmatiche

Affinché questo sistema di dipendenze funzioni, anziché limitarsi a commentare le istruzioni @import nel file manifest, la logica Sass dovrà dettare se i parziali verranno compilati o meno.

In src/scss/settings , crea un nuovo parziale chiamato _imports.scss , @import it in settings/_core.scss , quindi crea la seguente mappa SCSS:

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' ) );

Questa mappa avrà lo stesso ruolo del manifest di importazione nell'esempio 1.

Esempio 4

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

Dovrebbe comportarsi come un set standard di @imports , in quanto se alcuni parziali sono commentati (come sopra), quel codice non dovrebbe essere compilato su build.

Ma poiché vogliamo importare automaticamente le dipendenze, dovremmo anche essere in grado di ignorare questa mappa nelle giuste circostanze.

Render Mixin

Iniziamo ad aggiungere un po' di logica Sass. Crea _render.scss in src/scss/tools , quindi aggiungi il suo @import a tools/_core.scss .

Nel file, crea un mixin vuoto chiamato render() .

src/scss/tools/_render.scss

 @mixin render() { }

Nel mixin, dobbiamo scrivere Sass che fa quanto segue:

  • rendere()
    “Ehi, $imports , bel tempo vero? Dì, hai l'oggetto contenitore nella tua mappa?"
  • $ importazioni
    false
  • rendere()
    “È un peccato, sembra che non verrà compilato in quel momento. Che ne dici del componente pulsante?"
  • $ importazioni
    true
  • rendere()
    "Carino! Questo è il pulsante in fase di compilazione. Saluta la moglie da parte mia".

In Sass, questo si traduce nel seguente:

src/scss/tools/_render.scss

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

Fondamentalmente, controlla se il parziale è incluso nella variabile $imports imports e, in tal caso, esegui il rendering utilizzando la direttiva @content di Sass, che ci consente di passare un blocco di contenuto nel mixin.

Lo useremmo così:

Esempio 5

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

Prima di usare questo mixin, c'è un piccolo miglioramento che possiamo apportare. Il nome del livello (oggetto, componente, utilità, ecc.) è qualcosa che possiamo prevedere in sicurezza, quindi abbiamo l'opportunità di semplificare un po' le cose.

Prima della dichiarazione render mixin, crea una variabile chiamata $layer e rimuovi la variabile con lo stesso nome dai parametri mixins. Così:

src/scss/tools/_render.scss

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

Ora, nei parziali _core.scss in cui si trovano oggetti, componenti e utilità @imports , dichiara nuovamente queste variabili ai seguenti valori; che rappresenta il tipo di classi CSS da importare.

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';

In questo modo, quando utilizziamo render() mixin, tutto ciò che dobbiamo fare è dichiarare il nome parziale.

Avvolgi il mixin render() attorno a ogni oggetto, componente e dichiarazione di classe di utilità, come di seguito. Questo ti darà un utilizzo del mix di rendering per parziale.

Per esempio:

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 }

Nota: per utilities/_widths.scss , avvolgere la funzione render() attorno all'intero parziale comporterà un errore durante la compilazione, poiché in Sass non è possibile annidare le dichiarazioni mixin all'interno delle chiamate mixin. Invece, avvolgi semplicemente il mixin render() attorno alle chiamate create-widths() , come di seguito:

 @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 }

Con questo in atto, in build, verranno compilati solo i parziali a cui si fa riferimento in $imports imports.

Mescola e abbina quali componenti sono commentati in $imports imports ed esegui npm run build nel terminale per provarlo.

Mappa delle dipendenze

Ora stiamo importando a livello di codice i parziali, possiamo iniziare a implementare la logica delle dipendenze.

In src/scss/settings , crea un nuovo parziale chiamato _dependencies.scss , @import it in settings/_core.scss , ma assicurati che sia dopo _imports.scss . Quindi in esso, crea la seguente mappa SCSS:

src/scss/settings/_dependencies.scss

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

Qui dichiariamo le dipendenze per il componente expander poiché richiede stili di altri parziali per il rendering corretto, come si vede in dist/index.html.

Usando questo elenco, possiamo scrivere una logica che significherebbe che queste dipendenze sarebbero sempre compilate insieme ai loro componenti dipendenti, indipendentemente dallo stato della variabile $imports .

Sotto $dependencies , crea un mixin chiamato dependency-setup() . Qui faremo le seguenti azioni:

1. Scorri la mappa delle dipendenze.

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

2. Se il componente può essere trovato in $imports , scorrere il suo elenco di dipendenze.

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

3. Se la dipendenza non è in $imports , aggiungila.

 @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; } } } } } }

Includere il flag !global dice a Sass di cercare la variabile $imports nell'ambito globale, piuttosto che nell'ambito locale del mixin.

4. Quindi è solo questione di chiamare il mixin.

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

Quindi quello che abbiamo ora è un sistema di importazione parziale migliorato, in cui se un componente viene importato, uno sviluppatore non deve quindi importare manualmente anche ciascuno dei suoi vari parziali di dipendenza.

Configurare la variabile $imports imports in modo che venga importato solo il componente di espansione, quindi eseguire npm run build . Dovresti vedere nel CSS compilato le classi di espansione insieme a tutte le sue dipendenze.

Tuttavia, questo non porta davvero nulla di nuovo sul tavolo in termini di filtraggio dei CSS inutilizzati, poiché la stessa quantità di Sass viene ancora importata, programmatica o meno. Miglioriamo su questo.

Importazione delle dipendenze migliorata

Un componente può richiedere solo una singola classe da una dipendenza, quindi continuare e importare tutte le classi di quella dipendenza porta solo allo stesso rigonfiamento non necessario che stiamo cercando di evitare.

Possiamo perfezionare il sistema per consentire un filtraggio più granulare classe per classe, per assicurarci che i componenti siano compilati solo con le classi di dipendenza di cui hanno bisogno.

Con la maggior parte dei modelli di progettazione, decorati o meno, esiste un numero minimo di classi che devono essere presenti nel foglio di stile affinché il modello venga visualizzato correttamente.

Per i nomi delle classi che utilizzano una convenzione di denominazione consolidata come BEM, in genere sono richieste almeno le classi denominate "Blocco" ed "Elemento", con i "Modificatori" in genere facoltativi.

Nota: le classi di utilità in genere non seguirebbero il percorso BEM, poiché sono isolate in natura a causa della loro focalizzazione ristretta.

Ad esempio, dai un'occhiata a questo oggetto multimediale, che è probabilmente l'esempio più noto di CSS orientato agli oggetti:

 <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>

Se un componente ha questo set come dipendenza, ha senso compilare sempre .o-media , .o-media__image e .o-media__text , poiché questa è la quantità minima di CSS richiesta per far funzionare il pattern. Tuttavia .o-media--spacing-small è un modificatore opzionale, dovrebbe essere compilato solo se lo diciamo esplicitamente, poiché il suo utilizzo potrebbe non essere coerente in tutte le istanze di oggetti multimediali.

Modificheremo la struttura della mappa $dependencies per permetterci di importare queste classi opzionali, includendo un modo per importare solo il blocco e l'elemento nel caso in cui non siano richiesti modificatori.

Per iniziare, controlla l'HTML di espansione in dist/index.html e prendi nota di tutte le classi di dipendenza in uso. Registrali nella mappa $dependencies , come di seguito:

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' ) ) ) );

Laddove un valore è impostato su true, lo tradurremo in "Compila solo classi a livello di blocco ed elemento, nessun modificatore!".

Il passaggio successivo prevede la creazione di una variabile di whitelist per archiviare queste classi e qualsiasi altra classe (non dipendente) che desideriamo importare manualmente. In /src/scss/settings/imports.scss , dopo $imports imports , crea un nuovo elenco Sass chiamato $global-filter .

src/scss/settings/_imports.scss

 $global-filter: ();

La premessa di base alla base $global-filter è che tutte le classi archiviate qui verranno compilate su build purché il parziale a cui appartengono sia importato tramite $imports .

Questi nomi di classe possono essere aggiunti a livello di codice se sono una dipendenza di un componente o possono essere aggiunti manualmente quando la variabile viene dichiarata, come nell'esempio seguente:

Esempio di filtro globale

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

Successivamente, dobbiamo aggiungere un po' più di logica al mixin @dependency-setup , quindi tutte le classi a cui si fa riferimento in $dependencies vengono automaticamente aggiunte alla nostra whitelist $global-filter .

Sotto questo blocco:

src/scss/settings/_dependencies.scss

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

...aggiungi il seguente snippet.

src/scss/settings/_dependencies.scss

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

Questo scorre tutte le classi di dipendenza e le aggiunge alla whitelist $global-filter .

A questo punto, se aggiungi un'istruzione @debug sotto il mixin dependency-setup() per stampare il contenuto di $global-filter nel terminale:

 @debug $global-filter;

...dovresti vedere qualcosa del genere su build:

 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"

Ora che abbiamo una whitelist di classe, dobbiamo applicarla a tutti i diversi parziali di oggetti, componenti e utilità.

Crea un nuovo parziale chiamato _filter.scss in src/scss/tools e aggiungi un @import al file _core.scss del livello strumenti.

In questo nuovo parziale creeremo un mixin chiamato filter() . Lo useremo per applicare la logica, il che significa che le classi verranno compilate solo se incluse nella variabile $global-filter .

Iniziando semplicemente, crea un mixin che accetti un singolo parametro: la $class controllata dal filtro. Successivamente, se la $class è inclusa nella whitelist $global-filter , consentirne la compilazione.

src/scss/tools/_filter.scss

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

In un parziale, avvolgeremmo il mixin attorno a una classe opzionale, in questo modo:

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

Ciò significa che la .o-myobject--modifier verrebbe compilata solo se inclusa in $global-filter , che può essere impostato direttamente o indirettamente tramite ciò che è impostato in $dependencies .

Passa attraverso il repository e applica filter() mixin a tutte le classi di modificatori facoltative tra i livelli di oggetti e componenti. Quando si gestisce il componente tipografico o il livello di utilità, poiché ogni classe è indipendente dall'altra, avrebbe senso renderle tutte opzionali, in modo da poter abilitare le classi quando ne abbiamo bisogno.

Ecco alcuni esempi:

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; } } } }

Nota: quando si aggiungono i nomi delle classi del suffisso reattivo al filter() mixin, non è necessario sfuggire al simbolo '@' con un '\'.

Durante questo processo, applicando filter() ai parziali, potresti (o meno) aver notato alcune cose.

Classi raggruppate

Alcune classi nella codebase sono raggruppate e condividono gli stessi stili, ad esempio:

src/scss/objects/_box.scss

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

Poiché il filtro accetta solo una singola classe, non tiene conto della possibilità che un blocco di dichiarazione di stile possa essere per più di una classe.

Per tenere conto di ciò, espanderemo filter() mixin così oltre a una singola classe, è in grado di accettare un arglista Sass contenente molte classi. Così:

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; } }

Quindi dobbiamo dire al filter() mixin che se una di queste classi è nel $global-filter , puoi compilare le classi.

Ciò comporterà una logica aggiuntiva per digitare check dell'argomento $class del mixin, rispondendo con un ciclo se viene passato un arglist per verificare se ogni elemento è nella variabile $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; } }

Quindi è solo questione di tornare ai seguenti parziali per applicare correttamente il filter() mixin:

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

A questo punto, torna a $imports e abilita solo il componente expander. Nel foglio di stile compilato, oltre agli stili dei livelli generico ed elementi, dovresti vedere solo quanto segue:

  • Le classi di blocchi ed elementi appartenenti al componente expander, ma non al suo modificatore.
  • Le classi di blocchi e di elementi appartenenti alle dipendenze dell'expander.
  • Qualsiasi classe di modifica che appartiene alle dipendenze dell'expander che è esplicitamente dichiarata nella variabile $dependencies .

Teoricamente, se hai deciso di voler includere più classi nel foglio di stile compilato, come il modificatore dei componenti dell'expander, è solo questione di aggiungerlo alla variabile $global-filter al momento della dichiarazione, o aggiungerlo in qualche altro punto nella codebase (purché sia ​​prima del punto in cui viene dichiarato il modificatore stesso).

Abilitando tutto

Quindi ora abbiamo un sistema abbastanza completo, che ti consente di importare oggetti, componenti e utilità fino alle singole classi all'interno di questi parziali.

Durante lo sviluppo, per qualsiasi motivo, potresti voler abilitare tutto in una volta sola. Per consentire ciò, creeremo una nuova variabile chiamata $enable-all-classes , quindi aggiungeremo una logica aggiuntiva, quindi se questo è impostato su true, tutto viene compilato indipendentemente dallo stato di $imports imports e $global-filter variabili di $global-filter .

Innanzitutto, dichiara la variabile nel nostro file manifest principale:

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';

Quindi dobbiamo solo apportare alcune modifiche minori ai nostri mixin filter() e render() per aggiungere una logica di override per quando la variabile $enable-all-classes è impostata su true.

Per prima cosa, il filter() mixin. Prima di qualsiasi controllo esistente, aggiungeremo un'istruzione @if per vedere se $enable-all-classes è impostata su true e, in tal caso, renderizzare @content , senza fare domande.

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; } }

Successivamente nel mixin render() , dobbiamo solo fare un controllo per vedere se la variabile $enable-all-classes è veritiera e, in tal caso, saltare qualsiasi ulteriore controllo.

src/scss/tools/_render.scss

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

Quindi ora, se dovessi impostare la variabile $enable-all-classes su true e ricostruire, ogni classe opzionale verrebbe compilata, risparmiando un bel po' di tempo nel processo.

Confronti

Per vedere che tipo di guadagni ci sta dando questa tecnica, eseguiamo alcuni confronti e vediamo quali sono le differenze di dimensione dei file.

Per assicurarci che il confronto sia corretto, dovremmo aggiungere gli oggetti box e container in $imports , quindi aggiungere il modificatore o-box--spacing-regular della scatola al $global-filter , in questo modo:

src/scss/settings/_imports.scss

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

Questo assicura che gli stili per gli elementi padre dell'expander vengano compilati come se non ci fosse alcun filtro in atto.

Fogli di stile originali e filtrati

Confrontiamo il foglio di stile originale con tutte le classi compilate, con il foglio di stile filtrato in cui è stato compilato solo il CSS richiesto dal componente expander.

Standard
Foglio di stile Dimensioni (kb) Taglia (gzip)
Originale 54,6 kb 6,98 kb
Filtrato 15.34kb (72% più piccolo) 4.91kb (29% più piccolo)
  • Originale: https://webdevluke.github.io/handlingunusedcss/dist/index2.html
  • Filtrato: https://webdevluke.github.io/handlingunusedcss/dist/index.html

Potresti pensare che la percentuale di risparmio di gzip significhi che questo non vale la pena, poiché non c'è molta differenza tra i fogli di stile originali e filtrati.

Vale la pena sottolineare che la compressione gzip funziona meglio con file più grandi e ripetitivi. Poiché il foglio di stile filtrato è l'unica prova di concetto e contiene solo CSS per il componente expander, non c'è tanto da comprimere come ci sarebbe in un progetto reale.

Se dovessimo aumentare ogni foglio di stile di un fattore 10 a dimensioni più tipiche della dimensione del bundle CSS di un sito Web, la differenza nelle dimensioni dei file gzip sarebbe molto più impressionante.

Dimensioni 10x
Foglio di stile Dimensioni (kb) Taglia (gzip)
Originale (10x) 892.07kb 75.70kb
Filtrato (10x) 209.45kb (77% più piccolo) 19.47kb (74% più piccolo)

Foglio di stile filtrato vs UNCSS

Ecco un confronto tra il foglio di stile filtrato e un foglio di stile che è stato eseguito tramite lo strumento UNCSS.

Filtrato vs UNCSS
Foglio di stile Dimensioni (kb) Taglia (gzip)
Filtrato 15.34kb 4.91kb
UNCSS 12.89kb (16% più piccolo) 4,25kb (13% più piccolo)

Lo strumento UNCSS vince qui marginalmente, poiché filtra i CSS nelle directory generiche ed elementi.

È possibile che su un sito Web reale, con una maggiore varietà di elementi HTML in uso, la differenza tra i 2 metodi sia trascurabile.

Avvolgendo

Quindi abbiamo visto come, usando solo Sass, puoi ottenere un maggiore controllo su quali classi CSS vengono compilate su build. Ciò riduce la quantità di CSS inutilizzati nel foglio di stile finale e accelera il percorso di rendering critico.

All'inizio dell'articolo, ho elencato alcuni inconvenienti di soluzioni esistenti come UNCSS. È giusto criticare allo stesso modo questa soluzione orientata a Sass, quindi tutti i fatti sono sul tavolo prima di decidere quale approccio è meglio per te:

Professionisti

  • Non sono richieste dipendenze aggiuntive, quindi non devi fare affidamento sul codice di qualcun altro.
  • Meno tempo di compilazione richiesto rispetto alle alternative basate su Node.js, poiché non è necessario eseguire browser headless per controllare il codice. Ciò è particolarmente utile con l'integrazione continua poiché è meno probabile che tu veda una coda di build.
  • Risulta in dimensioni di file simili rispetto agli strumenti automatizzati.
  • Immediatamente, hai il controllo completo su quale codice viene filtrato, indipendentemente da come quelle classi CSS vengono utilizzate nel tuo codice. Con le alternative basate su Node.js, spesso devi mantenere una whitelist separata in modo che le classi CSS appartenenti a HTML iniettato dinamicamente non vengano filtrate.

contro

  • La soluzione orientata a Sass è decisamente più pratica, nel senso che devi tenere il passo con le variabili $imports e $global-filter . Al di là della configurazione iniziale, le alternative a Node.js che abbiamo esaminato sono in gran parte automatizzate.
  • Se aggiungi classi CSS a $global-filter e poi le rimuovi dal tuo HTML, devi ricordarti di aggiornare la variabile, altrimenti compilerai CSS che non ti servono. Con progetti di grandi dimensioni su cui lavorano più sviluppatori contemporaneamente, questo potrebbe non essere facile da gestire a meno che non lo si pianifichi adeguatamente.
  • Non consiglierei di imbullonare questo sistema a nessuna base di codice CSS esistente, poiché dovresti dedicare un bel po' di tempo a mettere insieme le dipendenze e ad applicare render() mixin a MOLTE classi. È un sistema molto più facile da implementare con nuove build, in cui non hai codice esistente con cui fare i conti.

Spero che tu l'abbia trovato interessante da leggere come ho trovato interessante da mettere insieme. Se hai suggerimenti, idee per migliorare questo approccio o vuoi sottolineare qualche difetto fatale che ho completamente perso, assicurati di pubblicare nei commenti qui sotto.