Gestionarea CSS neutilizată în Sass pentru a îmbunătăți performanța
Publicat: 2022-03-10Î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.
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.
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.
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.
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.
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:
- Pe bulld, site-ul web este accesat printr-un browser fără cap (de exemplu: puppeteer) sau prin emulare DOM (de exemplu: jsdom).
- Pe baza elementelor HTML ale paginii, orice CSS neutilizat este identificat.
- 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.
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.