Obsługa nieużywanego CSS w Sass w celu poprawy wydajności
Opublikowany: 2022-03-10We współczesnym rozwoju front-end programiści powinni dążyć do pisania CSS, który jest skalowalny i łatwy w utrzymaniu. W przeciwnym razie ryzykują utratę kontroli nad szczegółami, takimi jak kaskada i specyficzność selektora, w miarę wzrostu bazy kodu i wkładu większej liczby programistów.
Jednym ze sposobów, w jaki można to osiągnąć, jest zastosowanie metodologii, takich jak CSS zorientowany obiektowo (OOCSS), który zamiast organizować CSS wokół kontekstu strony, zachęca do oddzielania struktury (systemy siatki, odstępy, szerokości itp.) od dekoracji (czcionki, marka, kolory itp.).
Więc nazwy klas CSS, takie jak:
-
.blog-right-column
-
.latest_topics_list
-
.job-vacancy-ad
Są zastępowane alternatywami wielokrotnego użytku, które stosują te same style CSS, ale nie są powiązane z żadnym konkretnym kontekstem:
-
.col-md-4
-
.list-group
-
.card
Takie podejście jest powszechnie wdrażane za pomocą frameworka Sass, takiego jak Bootstrap, Foundation lub coraz częściej frameworka na zamówienie, który można kształtować tak, aby lepiej pasował do projektu.
Więc teraz używamy klas CSS, które zostały wybrane ze schematu wzorców, komponentów UI i klas narzędziowych. Poniższy przykład ilustruje powszechny system siatki zbudowany przy użyciu Bootstrap, który układa się pionowo, a następnie po osiągnięciu punktu przerwania md przełącza się na układ 3 kolumnowy.
<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>
Klasy generowane programowo, takie jak .col-12
i .col-md-4
są tutaj używane do tworzenia tego wzorca. Ale co z .col-1
do .col-11
, .col-lg-4
, .col-md-6
lub .col-sm-12
? To wszystko są przykłady klas, które znajdą się w skompilowanym arkuszu stylów CSS, pobrane i przeanalizowane przez przeglądarkę, mimo że nie są używane.
W tym artykule zaczniemy od zbadania wpływu, jaki nieużywany CSS może mieć na szybkość ładowania strony. Następnie dotkniemy istniejącego rozwiązania do usuwania go z arkuszy stylów, kontynuując moje własne rozwiązanie zorientowane na Sassa.
Mierzenie wpływu nieużywanych klas CSS
Podczas gdy uwielbiam Sheffield United, potężne ostrza, CSS ich witryny jest spakowany w pojedynczy, zminimalizowany plik o wielkości 568 kb, który osiąga 105 kb nawet po spakowaniu gzipem. Wydaje się, że to dużo.

Czy zobaczymy, ile tego CSS faktycznie wykorzystuje na ich stronie głównej? Szybkie wyszukiwanie w Google ujawnia mnóstwo narzędzi online do pracy, ale wolę używać narzędzia do obsługi zasięgu w Chrome, które można uruchomić bezpośrednio z DevTools Chrome. Dajmy temu wir.

coverage
i wybierz opcję „Pokaż zasięg”. (duży podgląd)Wyniki pokazują, że tylko 30 KB kodu CSS z arkusza stylów 568 KB jest używane przez stronę główną, a pozostałe 538 KB dotyczy stylów wymaganych dla reszty witryny. Oznacza to, że aż 94,8% CSS jest niewykorzystanych.

CSS jest częścią krytycznej ścieżki renderowania strony internetowej, która obejmuje wszystkie kroki, które musi wykonać przeglądarka, zanim będzie mogła rozpocząć renderowanie strony. To sprawia, że CSS jest zasobem blokującym renderowanie.
Mając to na uwadze, podczas ładowania witryny Sheffield United przy użyciu dobrego połączenia 3G, pobranie CSS i rozpoczęcie renderowania stron zajmuje całe 1,15 s. To jest problem.
Google również to rozpoznało. Podczas przeprowadzania audytu Lighthouse, online lub przez przeglądarkę, wszelkie potencjalne oszczędności czasu ładowania i rozmiaru plików, które można uzyskać, usuwając nieużywane CSS, są podświetlone.

Istniejące rozwiązania
Celem jest określenie, które klasy CSS nie są wymagane i usunięcie ich z arkusza stylów. Dostępne są istniejące rozwiązania, które próbują zautomatyzować ten proces. Zwykle można ich używać za pośrednictwem skryptu kompilacji Node.js lub programów do uruchamiania zadań, takich jak Gulp. Obejmują one:
- UNCSS
- OczyśćCSS
- UsuńCSS
Działają one na ogół w podobny sposób:
- Na bulldzie, dostęp do strony odbywa się za pomocą przeglądarki bezgłowej (np. puppeteer) lub emulacji DOM (np.: jsdom).
- Na podstawie elementów HTML strony identyfikowany jest nieużywany kod CSS.
- To jest usuwane z arkusza stylów, pozostawiając tylko to, co jest potrzebne.
Chociaż te zautomatyzowane narzędzia są całkowicie poprawne i z powodzeniem korzystałem z wielu z nich w wielu komercyjnych projektach, napotkałem po drodze kilka wad, którymi warto się podzielić:
- Jeśli nazwy klas zawierają znaki specjalne, takie jak „@” lub „/”, mogą one nie zostać rozpoznane bez napisania niestandardowego kodu. Używam BEM-IT autorstwa Harry'ego Robertsa, który obejmuje strukturyzowanie nazw klas z responsywnymi sufiksami, takimi jak:
u-width-6/12@lg
, więc natknąłem się na ten problem już wcześniej. - Jeśli witryna korzysta z automatycznego wdrażania, może to spowolnić proces budowania, zwłaszcza jeśli masz dużo stron i dużo CSS.
- Wiedza na temat tych narzędzi musi być dzielona przez cały zespół, w przeciwnym razie może wystąpić zamieszanie i frustracja, gdy CSS jest tajemniczo nieobecny w arkuszach stylów produkcyjnych.
- Jeśli w Twojej witrynie działa wiele skryptów innych firm, czasami otwieranych w bezgłowej przeglądarce, nie działają one dobrze i mogą powodować błędy w procesie filtrowania. Dlatego zazwyczaj musisz napisać niestandardowy kod, aby wykluczyć wszelkie skrypty innych firm po wykryciu przeglądarki bezgłowej, co w zależności od konfiguracji może być trudne.
- Ogólnie rzecz biorąc, tego rodzaju narzędzia są skomplikowane i wprowadzają wiele dodatkowych zależności do procesu budowania. Podobnie jak w przypadku wszystkich zależności zewnętrznych, oznacza to poleganie na kodzie kogoś innego.
Mając to na uwadze, zadałem sobie pytanie:
Używając tylko Sassa, czy można lepiej obsługiwać Sass, który kompilujemy, aby każdy nieużywany CSS mógł zostać wykluczony, bez uciekania się do po prostu prymitywnego usuwania klas źródłowych w Sass?
Uwaga, spoiler: odpowiedź brzmi: tak. Oto, co wymyśliłem.
Rozwiązanie zorientowane na Sass
Rozwiązanie musi zapewniać szybki i łatwy sposób wybrania tego, co Sass powinien zostać skompilowany, a jednocześnie być na tyle proste, aby nie zwiększało złożoności procesu rozwoju ani nie uniemożliwiało programistom korzystania z takich rzeczy, jak programowo generowany CSS zajęcia.
Na początek jest repozytorium ze skryptami budowania i kilkoma przykładowymi stylami, które możesz sklonować stąd.
Wskazówka: Jeśli utkniesz, zawsze możesz powiązać z ukończoną wersją w gałęzi master.
cd
do repozytorium, uruchom npm install
, a następnie npm run build
, aby skompilować dowolny Sass do CSS zgodnie z wymaganiami. Powinno to utworzyć plik css o wielkości 55 kb w katalogu dist.
Jeśli następnie otworzysz /dist/index.html
w swojej przeglądarce internetowej, powinieneś zobaczyć dość standardowy komponent, który po kliknięciu rozwija się, aby odsłonić pewną zawartość. Możesz to również zobaczyć tutaj, gdzie zostaną zastosowane rzeczywiste warunki sieciowe, dzięki czemu możesz uruchomić własne testy.

Filtrowanie na poziomie częściowym
W typowej konfiguracji SCSS prawdopodobnie będziesz mieć jeden plik manifestu (np. main.scss
w repozytorium) lub jeden na stronę (np.: index.scss
, products.scss
, contact.scss
), gdzie częściowe frameworki są importowane. Zgodnie z zasadami OOCSS te importy mogą wyglądać mniej więcej tak:
Przykład 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';
Jeśli którykolwiek z tych podszablonów nie jest używany, naturalnym sposobem filtrowania tego nieużywanego CSS byłoby po prostu wyłączenie importu, co uniemożliwiłoby jego kompilację.
Na przykład, jeśli używa się tylko komponentu ekspandera, manifest będzie zwykle wyglądał jak poniżej:
Przykład 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';
Jednak, zgodnie z OOCSS, oddzielamy dekorację od struktury, aby umożliwić maksymalne ponowne użycie, więc możliwe jest, że ekspander może wymagać CSS od innych obiektów, komponentów lub klas narzędziowych do poprawnego renderowania. O ile programista nie jest świadomy tych relacji, sprawdzając kod HTML, może nie wiedzieć, jak zaimportować te części, więc nie wszystkie wymagane klasy zostaną skompilowane.
W repozytorium, jeśli spojrzysz na kod HTML ekspandera w dist/index.html
, wydaje się, że tak jest. Wykorzystuje style z obiektów ramki i układu, komponent typografii oraz narzędzia szerokości i wyrównania.
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>
Rozwiążmy ten problem, czekając na pojawienie się, czyniąc te relacje oficjalnymi w samym Sass, więc po zaimportowaniu komponentu wszelkie zależności zostaną również zaimportowane automatycznie. W ten sposób programista nie ponosi już dodatkowych kosztów związanych z audytem kodu HTML, aby dowiedzieć się, co jeszcze musi zaimportować.
Mapa zautomatyzowanego importu
Aby ten system zależności działał, zamiast po prostu komentować instrukcje @import
w pliku manifestu, logika Sassa będzie musiała dyktować, czy części będą kompilowane, czy nie.
W src/scss/settings
stwórz nową część o nazwie _imports.scss
, @import
ją w settings/_core.scss
, a następnie utwórz następującą mapę 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' ) );
Ta mapa będzie miała taką samą rolę jak manifest importu z przykładu 1.
Przykład 4
$imports: ( object: ( //'box', //'container', //'layout' ), component: ( //'button', 'expander', //'typography' ), utility: ( //'alignments', //'widths' ) );
Powinien zachowywać się jak standardowy zestaw @imports
, w tym przypadku, gdy pewne podszablony są zakomentowane (jak powyżej), wtedy ten kod nie powinien być kompilowany podczas kompilacji.
Ale ponieważ chcemy automatycznie importować zależności, powinniśmy również móc zignorować tę mapę w odpowiednich okolicznościach.
Mieszanie renderowania
Zacznijmy dodawać trochę logiki Sassa. Utwórz _render.scss
w src/scss/tools
, a następnie dodaj jego @import
do tools/_core.scss
.
W pliku utwórz pusty mixin o nazwie render()
.
src/scss/tools/_render.scss
@mixin render() { }
W domieszce musimy napisać Sassa, który wykonuje następujące czynności:
- renderowanie()
„Hej, tam$imports
, ładna pogoda, prawda? Powiedz, czy masz obiekt kontenera na swojej mapie?” - $import
false
- renderowanie()
„Szkoda, wygląda na to, że nie zostanie wtedy skompilowana. A co z komponentem przycisku?” - $import
true
- renderowanie()
"Miły! To jest przycisk, który jest wtedy kompilowany. Pozdrów ode mnie żonę.
W Sassie przekłada się to na:
src/scss/tools/_render.scss
@mixin render($name, $layer) { @if(index(map-get($imports, $layer), $name)) { @content; } }
Zasadniczo sprawdź, czy podszablon jest zawarty w zmiennej $imports
imports, a jeśli tak, wyrenderuj go za pomocą dyrektywy @content
, która pozwala nam przekazać blok zawartości do mixina.
Używalibyśmy go tak:
Przykład 5
@include render('button', 'component') { .c-button { // styles et al } // any other class declarations }
Przed użyciem tej mieszanki możemy wprowadzić do niej niewielką poprawę. Nazwę warstwy (obiekt, komponent, narzędzie itp.) możemy bezpiecznie przewidzieć, więc mamy okazję trochę usprawnić.
Przed deklaracją render mixin utwórz zmienną o nazwie $layer
i usuń identycznie nazwaną zmienną z parametrów mixins. Tak jak:
src/scss/tools/_render.scss
$layer: null !default; @mixin render($name) { @if(index(map-get($imports, $layer), $name)) { @content; } }
Teraz w części _core.scss
, w której znajdują się obiekty, komponenty i narzędzia @imports
, ponownie zadeklaruj te zmienne na następujące wartości; reprezentujący typ importowanych klas CSS.
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';
W ten sposób, kiedy używamy mixin render()
, wszystko, co musimy zrobić, to zadeklarować częściową nazwę.
Owiń domieszkę render()
wokół każdego obiektu, komponentu i deklaracji klasy narzędziowej, jak pokazano poniżej. To da ci jedno użycie domieszki renderującej na części.
Na przykład:
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 }
Uwaga: W przypadku utilities/_widths.scss
, zawinięcie funkcji render()
wokół całej części spowoduje błąd podczas kompilacji, ponieważ w Sassie nie można zagnieżdżać deklaracji mixin w wywołaniach mixin. Zamiast tego po prostu owiń mixin render()
wokół wywołań create-widths()
, jak poniżej:
@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 }
Mając to na miejscu, podczas kompilacji tylko części, do których odwołuje się $imports
, zostaną skompilowane.
Wymieszaj i dopasuj komponenty, które są zakomentowane w $imports
i uruchom npm run build
w terminalu, aby spróbować.
Mapa zależności
Teraz programowo importujemy części, możemy zacząć implementować logikę zależności.
W src/scss/settings
stwórz nową część o nazwie _dependencies.scss
, @import
ją w settings/_core.scss
, ale upewnij się, że jest po _imports.scss
. Następnie utwórz w nim następującą mapę SCSS:
src/scss/settings/_dependencies.scss
$dependencies: ( expander: ( object: ( 'box', 'layout' ), component: ( 'button', 'typography' ), utility: ( 'alignments', 'widths' ) ) );
Tutaj deklarujemy zależności dla komponentu ekspandera, ponieważ wymaga on stylów z innych części do poprawnego renderowania, jak widać w dist/index.html.

Korzystając z tej listy, możemy napisać logikę, która oznaczałaby, że te zależności będą zawsze kompilowane wraz z ich zależnymi komponentami, bez względu na stan zmiennej $imports
.
Poniżej $dependencies
stwórz mixin o nazwie dependency-setup()
. Tutaj wykonamy następujące czynności:
1. Przejdź przez mapę zależności.
@mixin dependency-setup() { @each $componentKey, $componentValue in $dependencies { } }
2. Jeśli komponent można znaleźć w $imports
, przejdź przez jego listę zależności.
@mixin dependency-setup() { $components: map-get($imports, component); @each $componentKey, $componentValue in $dependencies { @if(index($components, $componentKey)) { @each $layerKey, $layerValue in $componentValue { } } } }
3. Jeśli zależności nie ma w $imports
, dodaj ją.
@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; } } } } } }
Dołączenie flagi !global
mówi Sassowi, aby szukał zmiennej $imports
w zasięgu globalnym, a nie lokalnym zasięgu mixin.
4. Wtedy wystarczy tylko wywołać mixin.
@mixin dependency-setup() { ... } @include dependency-setup();
Mamy więc teraz ulepszony system częściowego importu, w którym jeśli komponent jest importowany, programista nie musi również ręcznie importować każdego z jego różnych częściowych zależności.
Skonfiguruj zmienną $imports
był tylko składnik ekspandera, a następnie uruchom npm run build
. Powinieneś zobaczyć w skompilowanym CSS klasy ekspandera wraz ze wszystkimi jego zależnościami.
Jednak nie wnosi to nic nowego do tabeli, jeśli chodzi o filtrowanie nieużywanego CSS, ponieważ ta sama ilość Sassa jest nadal importowana, zautomatyzowana lub nie. Poprawmy to.
Ulepszone importowanie zależności
Komponent może wymagać tylko jednej klasy z zależności, więc zaimportowanie wszystkich klas z tej zależności prowadzi do tego samego niepotrzebnego rozdęcia, którego staramy się uniknąć.
Możemy udoskonalić system, aby umożliwić bardziej szczegółowe filtrowanie na podstawie klasy po klasie, aby upewnić się, że komponenty są kompilowane tylko z wymaganymi klasami zależności.
W przypadku większości wzorców projektowych, dekorowanych lub nie, istnieje minimalna liczba klas, które muszą być obecne w arkuszu stylów, aby wzorzec wyświetlał się poprawnie.
W przypadku nazw klas korzystających z ustalonej konwencji nazewnictwa, takiej jak BEM, zazwyczaj wymagane są co najmniej klasy o nazwie „Blok” i „Element”, a „Modyfikatory” są zwykle opcjonalne.
Uwaga: Klasy użytkowe zwykle nie podążają ścieżką BEM, ponieważ są z natury odizolowane ze względu na ich wąski zakres.
Na przykład spójrz na ten obiekt multimedialny, który jest prawdopodobnie najbardziej znanym przykładem zorientowanego obiektowo CSS:
<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>
Jeśli komponent ma ten zestaw jako zależność, warto zawsze kompilować .o-media
, .o-media__image
i .o-media__text
, ponieważ jest to minimalna ilość CSS wymagana do działania wzorca. Jednak ponieważ .o-media--spacing-small
jest opcjonalnym modyfikatorem, należy go skompilować tylko wtedy, gdy wyraźnie to powiemy, ponieważ jego użycie może nie być spójne we wszystkich instancjach obiektu multimedialnego.
Zmodyfikujemy strukturę mapy $dependencies
, aby umożliwić nam zaimportowanie tych opcjonalnych klas, jednocześnie uwzględniając sposób importowania tylko bloku i elementu w przypadku, gdy modyfikatory nie są wymagane.
Aby rozpocząć, sprawdź kod HTML ekspandera w dist/index.html i zanotuj wszystkie używane klasy zależności. Zapisz je na mapie $dependencies
, jak poniżej:
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' ) ) ) );
Gdy wartość jest ustawiona na true, przetłumaczymy to na „Kompiluj tylko klasy na poziomie bloków i elementów, bez modyfikatorów!”.
Następny krok polega na utworzeniu zmiennej białej listy do przechowywania tych klas oraz wszelkich innych (nie zależnych) klas, które chcemy ręcznie zaimportować. W /src/scss/settings/imports.scss
, po $imports
imports , utwórz nową listę Sassa o nazwie $global-filter
.
src/scss/settings/_imports.scss
$global-filter: ();
Podstawową przesłanką stojącą za $global-filter
jest to, że wszelkie przechowywane tutaj klasy będą kompilowane podczas budowania, o ile podszablon, do którego należą, zostanie zaimportowany przez $imports
.
Te nazwy klas można dodać programowo, jeśli są zależnością komponentu, lub można je dodać ręcznie, gdy zmienna jest zadeklarowana, jak w poniższym przykładzie:
Przykład filtra globalnego
$global-filter: ( 'o-box--spacing-regular@md', 'u-align-center', 'u-width-6/12@lg' );
Następnie musimy dodać trochę więcej logiki do domieszki @dependency-setup
, aby wszelkie klasy, do których odwołuje się $dependencies
, były automatycznie dodawane do naszej białej listy $global-filter
.
Poniżej tego bloku:
src/scss/settings/_dependencies.scss
@if not index(map-get($imports, $layerKey), $partKey) { }
...dodaj następujący fragment.
src/scss/settings/_dependencies.scss
@each $class in $partValue { $global-filter: append($global-filter, '#{$class}', 'comma') !global; }
To przechodzi przez wszystkie klasy zależności i dodaje je do białej listy $global-filter
.
W tym momencie, jeśli dodasz instrukcję @debug
poniżej domieszki dependency-setup()
, aby wyświetlić zawartość $global-filter
w terminalu:
@debug $global-filter;
...powinieneś zobaczyć coś takiego na kompilacji:
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"
Teraz mamy białą listę klas, musimy wymusić to na wszystkich różnych podsegmentach obiektów, komponentów i narzędzi.
Utwórz nową część o nazwie _filter.scss
w src/scss/tools
i dodaj @import
do pliku _core.scss
warstwy narzędzi.
W tej nowej części utworzymy mixin o nazwie filter()
. Wykorzystamy to do zastosowania logiki, co oznacza, że klasy zostaną skompilowane tylko wtedy, gdy zostaną uwzględnione w zmiennej $global-filter
.
Zaczynając od prostego, utwórz mixin, który akceptuje pojedynczy parametr — $class
którą kontroluje filtr. Następnie, jeśli $class
znajduje się na białej liście $global-filter
, zezwól na jej kompilację.
src/scss/tools/_filter.scss
@mixin filter($class) { @if(index($global-filter, $class)) { @content; } }
W części owinęlibyśmy mixin wokół opcjonalnej klasy, tak jak poniżej:
@include filter('o-myobject--modifier') { .o-myobject--modifier { color: yellow; } }
Oznacza to, że klasa .o-myobject--modifier
zostałaby skompilowana tylko wtedy, gdyby była zawarta w $global-filter
, który można ustawić bezpośrednio lub pośrednio przez to, co jest ustawione w $dependencies
.
Przejdź przez repozytorium i zastosuj domieszkę filter()
do wszystkich opcjonalnych klas modyfikatorów na warstwach obiektu i składnika. Podczas obsługi komponentu typograficznego lub warstwy narzędziowej, ponieważ każda klasa jest niezależna od następnej, sensowne byłoby uczynienie ich wszystkich opcjonalnymi, abyśmy mogli po prostu włączyć klasy, których potrzebujemy.
Oto kilka przykładów:
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; } } } }
Uwaga: podczas dodawania responsywnych nazw klas sufiksów do domieszki filter()
nie musisz poprzedzać symbolu „@” znakiem „\”.
Podczas tego procesu, podczas stosowania domieszki filter()
do podszablonów, mogłeś (lub nie) zauważyć kilka rzeczy.
Zgrupowane klasy
Niektóre klasy w bazie kodu są zgrupowane razem i mają te same style, na przykład:
src/scss/objects/_box.scss
.o-box--spacing-disable-left, .o-box--spacing-horizontal { padding-left: 0; }
Ponieważ filtr akceptuje tylko jedną klasę, nie uwzględnia możliwości, że jeden blok deklaracji stylu może dotyczyć więcej niż jednej klasy.
Aby to uwzględnić, rozszerzymy mixin filter()
, aby oprócz pojedynczej klasy był w stanie zaakceptować listę arg Sass zawierającą wiele klas. Tak jak:
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; } }
Więc musimy powiedzieć mixin filter()
, że jeśli któraś z tych klas jest w $global-filter
, możesz skompilować klasy.
Będzie to wymagało dodatkowej logiki, aby sprawdzić argument $class
miksera, odpowiadając pętlą, jeśli lista argumentów zostanie przekazana, aby sprawdzić, czy każdy element znajduje się w zmiennej $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; } }
Następnie wystarczy wrócić do następujących podszablonów, aby poprawnie zastosować mixin filter()
:
-
objects/_box.scss
-
objects/_layout.scss
-
utilities/_alignments.scss
W tym momencie wróć do $imports
i włącz tylko komponent ekspandera. W skompilowanym arkuszu stylów, oprócz stylów z warstwy ogólnej i elementów, powinieneś zobaczyć tylko następujące elementy:
- Klasy bloku i elementu należące do komponentu ekspandera, ale nie jego modyfikatora.
- Klasy bloków i elementów należących do zależności ekspandera.
- Wszelkie klasy modyfikatorów należące do zależności ekspandera, które są jawnie zadeklarowane w zmiennej
$dependencies
.
Teoretycznie, jeśli zdecydujesz, że chcesz dołączyć więcej klas do skompilowanego arkusza stylów, na przykład modyfikator komponentów expandera, to wystarczy dodać go do zmiennej $global-filter
w momencie deklaracji lub dołączyć w innym miejscu w bazie kodu (o ile znajduje się przed punktem, w którym zadeklarowany jest sam modyfikator).
Włączenie wszystkiego
Mamy więc teraz całkiem kompletny system, który pozwala importować obiekty, komponenty i narzędzia do poszczególnych klas w tych podszablonach.
Podczas programowania, z jakiegokolwiek powodu, możesz po prostu włączyć wszystko za jednym razem. Aby to umożliwić, utworzymy nową zmienną o nazwie $enable-all-classes
, a następnie dodamy dodatkową logikę, więc jeśli jest ustawiona na true, wszystko zostanie skompilowane bez względu na stan $imports
imports i $global-filter
zmienne.
Najpierw zadeklaruj zmienną w naszym głównym pliku manifestu:
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';
Następnie wystarczy wprowadzić kilka drobnych zmian w naszych domieszkach filter()
i render()
, aby dodać logikę nadpisań, gdy zmienna $enable-all-classes
jest ustawiona na wartość true.
Najpierw mixin filter()
. Przed wszelkimi istniejącymi kontrolami dodamy instrukcję @if
, aby sprawdzić, czy $enable-all-classes
ma wartość true, a jeśli tak, wyrenderuj @content
, bez zadawania pytań.
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; } }
Następnie w domieszce render()
musimy tylko sprawdzić, czy zmienna $enable-all-classes
jest prawdziwa, a jeśli tak, pomiń dalsze sprawdzanie.
src/scss/tools/_render.scss
$layer: null !default; @mixin render($name) { @if($enable-all-classes or index(map-get($imports, $layer), $name)) { @content; } }
Więc teraz, gdybyś miał ustawić zmienną $enable-all-classes
na true i przebudować, każda opcjonalna klasa zostałaby skompilowana, oszczędzając sporo czasu w tym procesie.
Porównania
Aby zobaczyć, jakie korzyści daje nam ta technika, przeprowadźmy kilka porównań i zobaczmy, jakie są różnice w wielkości plików.
Aby upewnić się, że porównanie jest rzetelne, powinniśmy dodać obiekty box i container w $imports
, a następnie dodać modyfikator o-box--spacing-regular
do $global-filter
, w następujący sposób:
src/scss/settings/_imports.scss
$imports: ( object: ( 'box', 'container' // 'layout' ), component: ( // 'button', 'expander' // 'typography' ), utility: ( // 'alignments', // 'widths' ) ); $global-filter: ( 'o-box--spacing-regular' );
Daje to pewność, że style elementów nadrzędnych ekspandera są kompilowane tak, jak byłyby, gdyby nie było filtrowania.
Oryginalne a filtrowane arkusze stylów
Porównajmy oryginalny arkusz stylów ze wszystkimi skompilowanymi klasami z przefiltrowanym arkuszem stylów, w którym skompilowano tylko CSS wymagany przez komponent ekspandera.
Standard | ||
---|---|---|
Arkusz stylów | Rozmiar (kb) | Rozmiar (gzip) |
Oryginalny | 54.6kb | 6.98kb |
Przefiltrowany | 15,34 KB (72% mniej) | 4,91kb (29% mniejsze) |
- Oryginał: https://webdevluke.github.io/handlingunusedcss/dist/index2.html
- Filtrowane: https://webdevluke.github.io/handlingunusedcss/dist/index.html
Możesz pomyśleć, że procentowe oszczędności gzip oznaczają, że nie jest to warte wysiłku, ponieważ nie ma dużej różnicy między oryginalnym a filtrowanym arkuszem stylów.
Warto podkreślić, że kompresja gzip działa lepiej z większymi i bardziej powtarzalnymi plikami. Ponieważ przefiltrowany arkusz stylów jest jedynym dowodem koncepcji i zawiera tylko CSS dla komponentu ekspandera, nie ma tak dużo do skompresowania, jak w prawdziwym projekcie.
Gdybyśmy mieli skalować każdy arkusz stylów o współczynnik 10 do rozmiarów bardziej typowych dla rozmiaru pakietu CSS witryny, różnica w rozmiarach plików gzip byłaby znacznie bardziej imponująca.
10x rozmiar | ||
---|---|---|
Arkusz stylów | Rozmiar (kb) | Rozmiar (gzip) |
Oryginalny (10x) | 892.07kb | 75.70kb |
Filtrowane (10x) | 209,45 KB (77% mniej) | 19.47kb (74% mniejsze) |
Filtrowany arkusz stylów a UNCSS
Oto porównanie między przefiltrowanym arkuszem stylów a arkuszem stylów, który został uruchomiony przez narzędzie UNCSS.
Filtrowane a UNCSS | ||
---|---|---|
Arkusz stylów | Rozmiar (kb) | Rozmiar (gzip) |
Przefiltrowany | 15.34kb | 4.91kb |
UNCSS | 12.89kb (16% mniejszy) | 4.25kb (13% mniejszy) |
Narzędzie UNCSS wygrywa tutaj marginalnie, ponieważ odfiltrowuje CSS w katalogach ogólnych i elementów.
Możliwe, że na prawdziwej stronie internetowej, z większą różnorodnością użytych elementów HTML, różnica między tymi dwiema metodami byłaby znikoma.
Zawijanie
Widzieliśmy więc, jak — używając samego Sassa — możesz uzyskać większą kontrolę nad tym, jakie klasy CSS są kompilowane podczas budowania. Zmniejsza to ilość nieużywanego CSS w końcowym arkuszu stylów i przyspiesza krytyczną ścieżkę renderowania.
Na początku artykułu wymieniłem niektóre wady istniejących rozwiązań, takich jak UNCSS. Słuszne jest krytykowanie tego rozwiązania zorientowanego na Sassa w ten sam sposób, więc wszystkie fakty są na stole, zanim zdecydujesz, które podejście jest dla Ciebie lepsze:
Plusy
- Nie są wymagane żadne dodatkowe zależności, więc nie musisz polegać na kodzie kogoś innego.
- Wymagany jest krótszy czas kompilacji niż alternatywy oparte na Node.js, ponieważ nie musisz uruchamiać przeglądarek bezgłowych, aby kontrolować swój kod. Jest to szczególnie przydatne w przypadku ciągłej integracji, ponieważ może być mniej prawdopodobne, że zobaczysz kolejkę kompilacji.
- Daje podobny rozmiar pliku w porównaniu z narzędziami automatycznymi.
- Po wyjęciu z pudełka masz pełną kontrolę nad tym, jaki kod jest filtrowany, niezależnie od tego, jak te klasy CSS są używane w Twoim kodzie. W przypadku alternatyw opartych na Node.js często musisz utrzymywać osobną białą listę, aby klasy CSS należące do dynamicznie wstrzykiwanego kodu HTML nie były odfiltrowywane.
Cons
- Rozwiązanie zorientowane na Sassa jest zdecydowanie bardziej praktyczne, w tym sensie, że musisz być na bieżąco ze zmiennymi
$imports
i$global-filter
. Poza wstępną konfiguracją, alternatywy Node.js, którym się przyjrzeliśmy, są w dużej mierze zautomatyzowane. - Jeśli dodasz klasy CSS do
$global-filter
, a później usuniesz je z kodu HTML, musisz pamiętać o aktualizacji zmiennej, w przeciwnym razie będziesz kompilować CSS, którego nie potrzebujesz. Z dużymi projektami, nad którymi pracuje wielu programistów jednocześnie, może to nie być łatwe do zarządzania, chyba że odpowiednio to zaplanujesz. - Nie polecałbym łączenia tego systemu z żadną istniejącą bazą kodu CSS, ponieważ musiałbyś poświęcić sporo czasu na składanie zależności i stosowanie mixinu
render()
do DUŻO klas. Jest to system znacznie łatwiejszy do zaimplementowania w nowych kompilacjach, w których nie masz istniejącego kodu, z którym musisz się zmagać.
Mam nadzieję, że czytanie tego tekstu było dla Ciebie równie interesujące, jak dla mnie ciekawe zestawienie. Jeśli macie jakieś sugestie, pomysły na ulepszenie tego podejścia lub chcecie zwrócić uwagę na jakąś fatalną wadę, którą całkowicie przeoczyłem, napiszcie w komentarzach poniżej.