Gestionarea CSS neutilizată în Sass pentru a îmbunătăți performanța

Publicat: 2022-03-10
Rezumat rapid ↬ Știți ce impact are CSS neutilizat asupra performanței? Spoiler: Este mult! În acest articol, vom explora o soluție orientată spre Sass pentru a trata CSS neutilizat, evitând necesitatea dependențelor complicate Node.js care implică browsere fără header și emularea DOM.

În dezvoltarea front-end modernă, dezvoltatorii ar trebui să urmărească să scrie CSS care să fie scalabil și întreținut. În caz contrar, riscă să piardă controlul asupra specificului, cum ar fi cascada și specificitatea selectorului, pe măsură ce baza de cod crește și mai mulți dezvoltatori contribuie.

O modalitate prin care acest lucru poate fi realizat este prin utilizarea metodologiilor precum CSS orientat pe obiecte (OOCSS), care, mai degrabă decât organizarea CSS în jurul contextului paginii, încurajează separarea structurii (sisteme de grile, spațiere, lățimi etc.) de decor (fonturi, marca, culorile etc.).

Deci, nume de clasă CSS, cum ar fi:

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

Sunt înlocuite cu alternative mai reutilizabile, care aplică aceleași stiluri CSS, dar nu sunt legate de un anumit context:

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

Această abordare este implementată în mod obișnuit cu ajutorul unui cadru Sass, cum ar fi Bootstrap, Foundation, sau din ce în ce mai des, un cadru personalizat care poate fi modelat pentru a se potrivi mai bine proiectului.

Mai multe după săritură! Continuați să citiți mai jos ↓

Așa că acum folosim clase CSS alese dintr-un cadru de modele, componente UI și clase de utilitate. Exemplul de mai jos ilustrează un sistem de grilă comun construit folosind Bootstrap, care se stivuiește vertical, apoi odată ce punctul de întrerupere md este atins, trece la un aspect cu 3 coloane.

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

Clasele generate prin program, cum ar fi .col-12 și .col-md-4 sunt folosite aici pentru a crea acest model. Dar cum rămâne .col-1 prin .col-11 , .col-lg-4 , .col-md-6 sau .col-sm-12 ? Acestea sunt toate exemple de clase care vor fi incluse în foaia de stil CSS compilată, descărcate și analizate de browser, în ciuda faptului că nu sunt utilizate.

În acest articol, vom începe prin a explora impactul CSS neutilizat pe care îl poate avea asupra vitezei de încărcare a paginii. Vom atinge apoi o soluție existentă pentru eliminarea acesteia din foile de stil, urmând cu propria mea soluție orientată spre Sass.

Măsurarea impactului claselor CSS neutilizate

În timp ce ador Sheffield United, lamele puternice, CSS-ul site-ului lor este inclus într-un singur fișier micificat de 568 kb, care ajunge la 105 kb chiar și atunci când este gzipped. Asta pare mult.

Acesta este site-ul lui Sheffield United, echipa mea locală de fotbal (aceasta este Fotbal pentru voi mult în colonii). (Previzualizare mare)

Să vedem cât de mult din acest CSS este folosit de fapt pe pagina lor de pornire? O căutare rapidă pe Google dezvăluie o mulțime de instrumente online până la locul de muncă, dar prefer să folosesc instrumentul de acoperire în Chrome, care poate fi rulat direct din DevTools din Chrome. Să-i dăm un vârtej.

Cea mai rapidă modalitate de a accesa instrumentul de acoperire din Instrumentele pentru dezvoltatori este să utilizați comanda rapidă de la tastatură Control+Shift+P sau Command+Shift+P (Mac) pentru a deschide meniul de comandă. În el, tastați coverage și selectați opțiunea „Afișare acoperire”. (Previzualizare mare)

Rezultatele arată că numai 30 kb de CSS din foaia de stil de 568 kb sunt folosite de pagina principală, restul de 538 kb se referă la stilurile necesare pentru restul site-ului. Aceasta înseamnă că 94,8% din CSS este nefolosit.

Puteți vedea astfel de timpi pentru orice material din Chrome în Instrumente pentru dezvoltatori prin rețea -> Faceți clic pe materialul dvs. -> fila Timing. (Previzualizare mare)

CSS face parte din calea critică de randare a unei pagini web, care implică toți pașii diferiți pe care trebuie să îi parcurgă un browser înainte de a putea începe redarea paginii. Acest lucru face ca CSS să fie un activ care blochează redarea.

Deci, având în vedere acest lucru, atunci când încărcați site-ul web Sheffield United folosind o conexiune 3G bună, durează 1,15 secunde înainte ca CSS să fie descărcat și redarea paginii să poată începe. Aceasta este o problemă.

Google a recunoscut și acest lucru. Când rulați un audit Lighthouse, online sau prin browser, sunt evidențiate orice potențiale economii de timp de încărcare și dimensiunea fișierelor care ar putea fi realizate prin eliminarea CSS neutilizate.

În Chrome (și Chromium Edge), puteți corecta auditurile Google Lighthouse făcând clic pe fila Audit din instrumentele pentru dezvoltatori. (Previzualizare mare)

Soluții existente

Scopul este de a determina care clase CSS nu sunt necesare și de a le elimina din foaia de stil. Sunt disponibile soluții existente care încearcă să automatizeze acest proces. Ele pot fi utilizate în mod obișnuit printr-un script de compilare Node.js sau prin intermediul programelor de rulare a sarcinilor, cum ar fi Gulp. Acestea includ:

  • UNCSS
  • PurifyCSS
  • PurgeCSS

Acestea funcționează în general într-un mod similar:

  1. Pe bulld, site-ul web este accesat printr-un browser fără cap (de exemplu: puppeteer) sau prin emulare DOM (de exemplu: jsdom).
  2. Pe baza elementelor HTML ale paginii, orice CSS neutilizat este identificat.
  3. Acesta este eliminat din foaia de stil, lăsând doar ceea ce este necesar.

Deși aceste instrumente automate sunt perfect valabile și le-am folosit multe dintre ele într-un număr de proiecte comerciale cu succes, am întâlnit câteva dezavantaje pe parcurs, care merită împărtășite:

  • Dacă numele claselor conțin caractere speciale, cum ar fi „@” sau „/”, acestea pot să nu fie recunoscute fără a scrie un cod personalizat. Folosesc BEM-IT de Harry Roberts, care implică structurarea numelor de clasă cu sufixe receptive precum: u-width-6/12@lg , așa că am mai întâlnit această problemă.
  • Dacă site-ul web folosește implementare automată, poate încetini procesul de construire, mai ales dacă aveți multe pagini și multe CSS.
  • Cunoștințele despre aceste instrumente trebuie împărtășite în întreaga echipă, altfel ar putea exista confuzie și frustrare atunci când CSS lipsește în mod misterios în foile de stil de producție.
  • Dacă site-ul dvs. web rulează multe scripturi terță parte, uneori când sunt deschise într-un browser fără cap, acestea nu se joacă bine și pot provoca erori în procesul de filtrare. Deci, de obicei, trebuie să scrieți cod personalizat pentru a exclude orice scripturi terță parte atunci când este detectat un browser fără cap, ceea ce, în funcție de configurația dvs., poate fi dificil.
  • În general, acest tip de instrumente sunt complicate și introduc o mulțime de dependențe suplimentare în procesul de construire. Ca și în cazul tuturor dependențelor terțelor părți, aceasta înseamnă să te bazezi pe codul altcuiva.

Având în vedere aceste puncte, mi-am pus o întrebare:

Folosind doar Sass, este posibil să gestionăm mai bine Sass-ul pe care îl compilăm, astfel încât orice CSS neutilizat să poată fi exclus, fără a recurge la ștergerea simplă a claselor sursă din Sass?

Alertă spoiler: răspunsul este da. Iată cu ce am venit.

Soluție orientată spre sass

Soluția trebuie să ofere o modalitate rapidă și ușoară de a alege ce ar trebui să fie compilat Sass, fiind în același timp suficient de simplă încât să nu adauge mai multă complexitate procesului de dezvoltare sau să împiedice dezvoltatorii să profite de lucruri precum CSS generat programatic. clase.

Pentru a începe, există un depozit cu scripturi de compilare și câteva modele de stiluri pe care le puteți clona de aici.

Sfat: Dacă rămâneți blocat, puteți oricând să faceți referințe încrucișate cu versiunea finalizată pe ramura principală.

cd în repo, rulați npm install și apoi npm run build pentru a compila orice Sass în CSS, după cum este necesar. Acest lucru ar trebui să creeze un fișier css de 55 kb în directorul dist.

Dacă apoi deschideți /dist/index.html în browserul dvs. web, ar trebui să vedeți o componentă destul de standard, care la clic se extinde pentru a dezvălui conținut. Puteți vizualiza și aici, unde se vor aplica condiții reale de rețea, astfel încât să puteți rula propriile teste.

Vom folosi această componentă de expansiune a interfeței de utilizare ca subiect de testare atunci când dezvoltăm soluția orientată spre Sass pentru gestionarea CSS neutilizate. (Previzualizare mare)

Filtrarea la nivel parțial

Într-o configurare SCSS tipică, probabil că veți avea un singur fișier manifest (de exemplu: main.scss în depozit) sau unul pe pagină (de exemplu: index.scss , products.scss , contact.scss ) unde cadrul parțial sunt importate. Urmând principiile OOCSS, aceste importuri pot arăta cam așa:

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

Dacă oricare dintre aceste părți parțiale nu este în uz, atunci modalitatea naturală de filtrare a acestui CSS neutilizat ar fi să dezactivați doar importul, ceea ce ar împiedica compilarea acestuia.

De exemplu, dacă utilizați numai componenta de expandare, manifestul ar arăta de obicei ca mai jos:

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

Cu toate acestea, conform OOCSS, separăm decorarea de structură pentru a permite o reutilizare maximă, așa că este posibil ca extensia să solicite CSS de la alte obiecte, componente sau clase de utilitate pentru a reda corect. Cu excepția cazului în care dezvoltatorul este conștient de aceste relații prin inspectarea HTML-ului, este posibil să nu știe să importe aceste părți parțiale, așa că nu vor fi compilate toate clasele necesare.

În repo, dacă te uiți la HTML-ul extensiei în dist/index.html , acesta pare să fie cazul. Folosește stiluri din caseta și obiectele de aspect, componenta de tipografie și utilitarele de lățime și aliniere.

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>

Să abordăm această problemă care așteaptă să se întâmple prin oficializarea acestor relații în cadrul Sass în sine, astfel încât odată ce o componentă este importată, orice dependențe vor fi de asemenea importate automat. În acest fel, dezvoltatorul nu mai are costul suplimentar de a audita HTML-ul pentru a afla ce altceva trebuie să importe.

Harta importurilor programatice

Pentru ca acest sistem de dependență să funcționeze, în loc să comenteze pur și simplu în instrucțiunile @import din fișierul manifest, logica Sass va trebui să dicteze dacă părțile vor fi compilate sau nu.

În src/scss/settings , creați un nou parțial numit _imports.scss , @import it în settings/_core.scss , apoi creați următoarea hartă 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' ) );

Această hartă va avea același rol ca și manifestul de import din exemplul 1.

Exemplul 4

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

Ar trebui să se comporte ca un set standard de @imports , în sensul că, dacă anumite părți parțiale sunt comentate (cum ar fi cele de mai sus), atunci acel cod nu ar trebui să fie compilat la build.

Dar, deoarece dorim să importăm dependențe automat, ar trebui să putem ignora această hartă în circumstanțele potrivite.

Render Mixin

Să începem să adăugăm ceva logic Sass. Creați _render.scss în src/scss/tools , apoi adăugați @import său la tools/_core.scss .

În fișier, creați un mixin gol numit render() .

src/scss/tools/_render.scss

 @mixin render() { }

În mixin, trebuie să scriem Sass care face următoarele:

  • face()
    „Hei, $imports , vreme frumoasă, nu-i așa? Spuneți, aveți obiectul container pe harta dvs.?"
  • $imports
    false
  • face()
    „Este păcat, se pare că nu va fi compilat atunci. Ce zici de componenta buton?”
  • $imports
    true
  • face()
    "Grozav! Acesta este butonul care se compila atunci. Salutați-o pe soție pentru mine.”

În Sass, acest lucru se traduce prin următoarele:

src/scss/tools/_render.scss

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

Practic, verificați dacă parțialul este inclus în variabila $imports și, dacă da, redați-l folosind directiva Sass @content , care ne permite să trecem un bloc de conținut în mixin.

L-am folosi astfel:

Exemplul 5

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

Înainte de a utiliza acest mixin, există o mică îmbunătățire pe care o putem aduce. Numele stratului (obiect, componentă, utilitate etc.) este ceva pe care îl putem prezice în siguranță, așa că avem ocazia să simplificăm puțin lucrurile.

Înainte de declarația de randare mixin, creați o variabilă numită $layer și eliminați variabila cu nume identic din parametrii mixins. Ca astfel:

src/scss/tools/_render.scss

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

Acum, în parțialele _core.scss unde se află obiectele, componentele și utilitatea @imports , redeclarați aceste variabile la următoarele valori; reprezentând tipul de clase CSS importate.

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

În acest fel, atunci când folosim mixin-ul render() , tot ce trebuie să facem este să declarăm numele parțial.

Înfășurați mixul render() în jurul fiecărui obiect, componentă și declarație de clasă de utilitate, conform celor de mai jos. Acest lucru vă va oferi o utilizare a mixului de randare per parțial.

De exemplu:

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 }

Notă: Pentru utilities/_widths.scss , împachetarea funcției render() în jurul întregului parțial va genera o eroare la compilare, deoarece în Sass nu puteți imbrica declarații mixin în apelurile mixin. În schimb, înfășurați mixul render() în jurul apelurilor create-widths() , ca mai jos:

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

Cu acest lucru în loc, la compilare, vor fi compilate doar părțile la care se face referire în $imports .

Amestecați și potriviți ce componente sunt comentate în $imports și rulați npm run build în terminal pentru a încerca.

Harta dependențelor

Acum importăm programe parțiale, putem începe să implementăm logica dependenței.

În src/scss/settings , creați un nou parțial numit _dependencies.scss , @import it în settings/_core.scss , dar asigurați-vă că este după _imports.scss . Apoi, în ea, creați următoarea hartă SCSS:

src/scss/settings/_dependencies.scss

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

Aici, declarăm dependențe pentru componenta expander, deoarece necesită stiluri din alte părți parțiale pentru a reda corect, așa cum se vede în dist/index.html.

Folosind această listă, putem scrie logica, ceea ce ar însemna că aceste dependențe vor fi întotdeauna compilate împreună cu componentele lor dependente, indiferent de starea variabilei $imports .

Sub $dependencies , creați un mixin numit dependency-setup() . Aici, vom face următoarele acțiuni:

1. Parcurgeți harta dependențelor.

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

2. Dacă componenta poate fi găsită în $imports , parcurgeți lista de dependențe.

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

3. Dacă dependența nu este în $imports , adăugați-o.

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

Includerea flagului !global îi spune lui Sass să caute variabila $imports în domeniul global, mai degrabă decât în ​​domeniul local al mixin-ului.

4. Atunci este doar o chestiune de a chema mixin-ul.

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

Deci, ceea ce avem acum este un sistem îmbunătățit de import parțial, în care, dacă o componentă este importată, un dezvoltator nu trebuie să importe manual și fiecare dintre diferitele sale parțiale de dependență.

Configurați variabila $imports astfel încât să fie importată numai componenta expander și apoi rulați npm run build . Ar trebui să vedeți în CSS compilat clasele de expandare împreună cu toate dependențele sale.

Cu toate acestea, acest lucru nu aduce cu adevărat nimic nou la tabel în ceea ce privește filtrarea CSS-urilor neutilizate, deoarece aceeași cantitate de Sass este încă importată, programatic sau nu. Să îmbunătățim acest lucru.

Importarea dependenței îmbunătățită

O componentă poate necesita doar o singură clasă dintr-o dependență, așa că pentru a continua și a importa toate clasele acelei dependențe duce doar la aceeași umflare inutilă pe care încercăm să o evităm.

Putem rafina sistemul pentru a permite o filtrare mai granulară, clasă cu clasă, pentru a ne asigura că componentele sunt compilate doar cu clasele de dependență de care au nevoie.

Cu majoritatea modelelor de design, decorate sau nu, există o cantitate minimă de clase care trebuie să fie prezente în foaia de stil pentru ca modelul să fie afișat corect.

Pentru numele claselor care utilizează o convenție de denumire stabilită, cum ar fi BEM, de obicei, clasele numite „Block” și „Element” sunt necesare ca minim, „Modificatorii” fiind de obicei opționale.

Notă: Clasele de utilități nu ar urma de obicei traseul BEM, deoarece sunt izolate în natură datorită concentrării lor înguste.

De exemplu, aruncați o privire la acest obiect media, care este probabil cel mai cunoscut exemplu de CSS orientat pe obiecte:

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

Dacă o componentă are acest set ca dependență, este logic să compilați întotdeauna .o-media , .o-media__image și .o-media__text , deoarece aceasta este cantitatea minimă de CSS necesară pentru ca modelul să funcționeze. Cu toate acestea, având în vedere că .o-media--spacing-small este un modificator opțional, ar trebui să fie compilat doar dacă o spunem în mod explicit, deoarece utilizarea sa poate să nu fie consecventă în toate instanțele de obiect media.

Vom modifica structura hărții $dependencies pentru a ne permite să importăm aceste clase opționale, incluzând în același timp o modalitate de a importa numai blocul și elementul în cazul în care nu sunt necesari modificatori.

Pentru a începe, verificați extensia HTML în dist/index.html și notați orice clase de dependență în uz. Înregistrați-le în harta $dependencies , după cum se arată mai jos:

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

Acolo unde o valoare este setată la adevărat, vom traduce acest lucru în „Compilați doar clase la nivel de bloc și element, fără modificatori!”.

Următorul pas implică crearea unei variabile de listă albă pentru a stoca aceste clase și orice alte clase (nedependente) pe care dorim să le importam manual. În /src/scss/settings/imports.scss , după $imports , creați o nouă listă Sass numită $global-filter .

src/scss/settings/_imports.scss

 $global-filter: ();

Premisa de bază din spatele $global-filter este că orice clase stocate aici vor fi compilate pe build, atâta timp cât parțialul căruia îi aparțin este importat prin $imports .

Aceste nume de clasă pot fi adăugate programatic dacă sunt o dependență de componentă sau pot fi adăugate manual atunci când variabila este declarată, ca în exemplul de mai jos:

Exemplu de filtru global

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

În continuare, trebuie să adăugăm puțin mai multă logică mixului @dependency-setup , astfel încât orice clase la care se face referire în $dependencies sunt adăugate automat la lista noastră albă $global-filter .

Sub acest bloc:

src/scss/settings/_dependencies.scss

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

...adăugați următorul fragment.

src/scss/settings/_dependencies.scss

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

Aceasta trece prin toate clasele de dependență și le adaugă la lista albă $global-filter .

În acest moment, dacă adăugați o instrucțiune @debug sub mixin-ul dependency-setup() pentru a imprima conținutul $global-filter în terminal:

 @debug $global-filter;

...ar trebui să vedeți ceva de genul acesta la 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"

Acum avem o listă albă de clasă, trebuie să aplicăm acest lucru în toate diferitele partiale de obiect, componente și utilitate.

Creați un nou parțial numit _filter.scss în src/scss/tools și adăugați un @import în fișierul _core.scss al stratului de instrumente.

În acest nou parțial, vom crea un mixin numit filter() . Vom folosi acest lucru pentru a aplica logica, ceea ce înseamnă că clasele vor fi compilate numai dacă sunt incluse în variabila $global-filter .

Pornind de la simplu, creați un mixin care acceptă un singur parametru - $class pe care o controlează filtrul. Apoi, dacă $class este inclusă în lista albă $global-filter , permiteți compilarea acesteia.

src/scss/tools/_filter.scss

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

Într-un parțial, am încheia mixin-ul în jurul unei clase opționale, astfel:

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

Aceasta înseamnă că clasa .o-myobject--modifier va fi compilată numai dacă este inclusă în $global-filter , care poate fi setată fie direct, fie indirect prin ceea ce este setat în $dependencies .

Treceți prin repo și aplicați mixin-ul filter() la toate clasele de modificatori opționale pe straturi de obiecte și componente. Când manipulați componenta de tipografie sau stratul de utilități, deoarece fiecare clasă este independentă de următoarea, ar fi logic să le facem pe toate opționale, astfel încât apoi să putem activa clasele după cum avem nevoie de ele.

Iată câteva exemple:

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

Notă: Când adăugați nume de clasă sufixe receptive la mixin-ul filter() , nu trebuie să scăpați de simbolul „@” cu „\”.

În timpul acestui proces, în timp ce aplicați amestecul filter() la partiale, este posibil să fi observat (sau nu) câteva lucruri.

Clase grupate

Unele clase din baza de cod sunt grupate și împărtășesc aceleași stiluri, de exemplu:

src/scss/objects/_box.scss

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

Deoarece filtrul acceptă doar o singură clasă, nu ține cont de posibilitatea ca un bloc de declarare a stilului să fie pentru mai mult de o clasă.

Pentru a ține seama de acest lucru, vom extinde mixul filter() astfel încât, pe lângă o singură clasă, este capabil să accepte o listă de argumente Sass care conține multe clase. Ca astfel:

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

Așa că trebuie să spunem filter() mixin că, dacă oricare dintre aceste clase se află în $global-filter , vi se permite să compilați clasele.

Acest lucru va implica o logică suplimentară pentru a tip check argumentul $class al mixin-ului, răspunzând cu o buclă dacă o listă de argumente este transmisă pentru a verifica dacă fiecare element se află în variabila $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; } }

Apoi este doar o chestiune de a reveni la următoarele parțiale pentru a aplica corect mixul filter() :

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

În acest moment, reveniți la $imports și activați doar componenta expander. În foaia de stil compilată, pe lângă stilurile din straturile generice și elemente, ar trebui să vedeți doar următoarele:

  • Clasele de bloc și elemente aparținând componentei expander, dar nu modificatorul acesteia.
  • Clasele de bloc și elemente aparținând dependențelor expandatorului.
  • Orice clase modificatoare aparținând dependențelor expandatorului care sunt declarate explicit în variabila $dependencies .

Teoretic, dacă ați decis că doriți să includeți mai multe clase în foaia de stil compilată, cum ar fi modificatorul componentelor expander, este doar o chestiune de a-l adăuga la variabila $global-filter în punctul de declarare sau de a o adăuga într-un alt punct. în baza de cod (atâta timp cât este înainte de punctul în care este declarat modificatorul în sine).

Activarea Totului

Deci avem acum un sistem destul de complet, care vă permite să importați obiecte, componente și utilități până la clasele individuale din aceste parțiale.

În timpul dezvoltării, indiferent de motiv, poate doriți doar să activați totul dintr-o singură mișcare. Pentru a permite acest lucru, vom crea o nouă variabilă numită $enable-all-classes și apoi vom adăuga o logică suplimentară, astfel încât dacă aceasta este setată la adevărat, totul este compilat indiferent de starea $imports și $global-filter variabile de $global-filter .

Mai întâi, declarați variabila în fișierul manifest 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';

Apoi, trebuie doar să facem câteva modificări minore la mixin-urile noastre filter() și render() pentru a adăuga o logică de suprascriere pentru când variabila $enable-all-classes este setată la true.

În primul rând, filter() mixin. Înainte de orice verificări existente, vom adăuga o instrucțiune @if pentru a vedea dacă $enable-all-classes este setat la adevărat și, dacă da, redăm @content , fără întrebări.

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

În continuare, în mixul render() , trebuie doar să facem o verificare pentru a vedea dacă variabila $enable-all-classes este adevărată și, dacă da, să omitem orice alte verificări.

src/scss/tools/_render.scss

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

Deci, acum, dacă ar fi să setați variabila $enable-all-classes la true și să reconstruiți, fiecare clasă opțională ar fi compilată, economisindu-vă destul de mult timp în proces.

Comparații

Pentru a vedea ce tip de câștiguri ne oferă această tehnică, să facem câteva comparații și să vedem care sunt diferențele de dimensiunea fișierelor.

Pentru a ne asigura că comparația este corectă, ar trebui să adăugăm obiectele cutie și container în $imports , apoi adăugam modificatorul o-box--spacing-regular al casetei la $global-filter , astfel:

src/scss/settings/_imports.scss

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

Acest lucru asigură că stilurile pentru elementele părinte ale expandatorului sunt compilate așa cum ar fi dacă nu ar fi avut loc nicio filtrare.

Foi de stil originale vs filtrate

Să comparăm foaia de stil originală cu toate clasele compilate, cu foaia de stil filtrată în care a fost compilat doar CSS cerut de componenta expander.

Standard
Foaia de stil Dimensiune (kb) Dimensiune (gzip)
Original 54,6 kb 6,98 kb
Filtrată 15,34 kb (cu 72% mai mic) 4,91 kb (cu 29% mai mic)
  • Original: https://webdevluke.github.io/handlingunusedcss/dist/index2.html
  • Filtrat: https://webdevluke.github.io/handlingunusedcss/dist/index.html

S-ar putea să credeți că economiile procentuale gzip înseamnă că acest lucru nu merită efortul, deoarece nu există prea multă diferență între foile de stil originale și cele filtrate.

Merită să subliniem că compresia gzip funcționează mai bine cu fișiere mai mari și mai repetitive. Deoarece foaia de stil filtrată este singura dovadă a conceptului și conține doar CSS pentru componenta expander, nu există atât de mult de comprimat cât ar fi într-un proiect real.

Dacă ar fi să mărim fiecare foaie de stil cu un factor de 10 la dimensiuni mai tipice pentru dimensiunea pachetului CSS al unui site web, diferența dintre dimensiunile fișierelor gzip este mult mai impresionantă.

10x Dimensiune
Foaia de stil Dimensiune (kb) Dimensiune (gzip)
Original (10x) 892,07 kb 75,70 kb
Filtrat (10x) 209,45 kb (cu 77% mai mic) 19,47 kb (cu 74% mai mic)

Foaie de stil filtrată vs UNCSS

Iată o comparație între foaia de stil filtrată și o foaie de stil care a fost rulată prin instrumentul UNCSS.

Filtrat vs UNCSS
Foaia de stil Dimensiune (kb) Dimensiune (gzip)
Filtrată 15,34 kb 4,91 kb
UNCSS 12,89 kb (cu 16% mai mic) 4,25 kb (cu 13% mai mic)

Instrumentul UNCSS câștigă aici marginal, deoarece filtrează CSS în directoarele generice și elemente.

Este posibil ca pe un site web real, cu o varietate mai mare de elemente HTML în uz, diferența dintre cele 2 metode să fie neglijabilă.

Încheierea

Așa că am văzut cum, folosind doar Sass, puteți obține mai mult control asupra claselor CSS care sunt compilate la construcție. Acest lucru reduce cantitatea de CSS neutilizate în foaia de stil finală și accelerează calea critică de randare.

La începutul articolului, am enumerat câteva dezavantaje ale soluțiilor existente, cum ar fi UNCSS. Este corect să critici această soluție orientată spre Sass în același mod, așa că toate faptele sunt pe masă înainte de a decide care abordare este mai bună pentru tine:

Pro

  • Nu sunt necesare dependențe suplimentare, deci nu trebuie să vă bazați pe codul altcuiva.
  • Este nevoie de mai puțin timp de construcție decât alternativele bazate pe Node.js, deoarece nu trebuie să rulați browsere fără cap pentru a vă audita codul. Acest lucru este util în special în cazul integrării continue, deoarece este mai puțin probabil să vedeți o coadă de versiuni.
  • Rezultă o dimensiune de fișier similară în comparație cu instrumentele automate.
  • Din cutie, aveți control complet asupra codului care este filtrat, indiferent de modul în care sunt utilizate acele clase CSS în codul dvs. Cu alternativele bazate pe Node.js, adesea trebuie să mențineți o listă albă separată, astfel încât clasele CSS care aparțin HTML injectat dinamic să nu fie filtrate.

Contra

  • Soluția orientată spre Sass este cu siguranță mai practică, în sensul că trebuie să țineți cont de variabilele $imports și $global-filter . Dincolo de configurarea inițială, alternativele Node.js pe care le-am analizat sunt în mare măsură automatizate.
  • Dacă adăugați clase CSS la $global-filter și apoi le eliminați din HTML, trebuie să vă amintiți să actualizați variabila, altfel veți compila CSS de care nu aveți nevoie. Având în vedere proiecte mari la care lucrează mai mulți dezvoltatori la un moment dat, acest lucru poate să nu fie ușor de gestionat dacă nu vă planificați în mod corespunzător.
  • Nu aș recomanda fixarea acestui sistem pe orice bază de cod CSS existentă, deoarece ar trebui să petreceți destul de mult timp reunind dependențe și aplică mixin-ul render() la o MULTE de clase. Este un sistem mult mai ușor de implementat cu versiuni noi, în care nu aveți cod existent cu care să vă luptați.

Sper că ați găsit acest lucru la fel de interesant de citit pe cât mi s-a părut interesant de adunat. Dacă aveți sugestii, idei pentru a îmbunătăți această abordare sau doriți să subliniați un defect fatal pe care l-am ratat în totalitate, asigurați-vă că postați în comentariile de mai jos.