Umgang mit nicht verwendetem CSS in Sass zur Verbesserung der Leistung
Veröffentlicht: 2022-03-10In der modernen Front-End-Entwicklung sollten Entwickler darauf abzielen, CSS zu schreiben, das skalierbar und wartbar ist. Andernfalls riskieren sie, die Kontrolle über Besonderheiten wie die Kaskaden- und Selektorspezifität zu verlieren, wenn die Codebasis wächst und mehr Entwickler beitragen.
Eine Möglichkeit, dies zu erreichen, ist die Verwendung von Methoden wie objektorientiertes CSS (OOCSS), das CSS nicht um den Seitenkontext herum organisiert, sondern die Trennung von Struktur (Rastersysteme, Abstände, Breite usw.) von Dekoration (Schriftarten, Marke, Farben usw.).
Also CSS-Klassennamen wie:
-
.blog-right-column
-
.latest_topics_list
-
.job-vacancy-ad
durch wiederverwendbarere Alternativen ersetzt werden, die die gleichen CSS-Stile anwenden, aber nicht an einen bestimmten Kontext gebunden sind:
-
.col-md-4
-
.list-group
-
.card
Dieser Ansatz wird üblicherweise mit Hilfe eines Sass-Frameworks wie Bootstrap, Foundation oder immer häufiger eines maßgeschneiderten Frameworks implementiert, das so geformt werden kann, dass es besser zum Projekt passt.
Also verwenden wir jetzt CSS-Klassen, die aus einem Framework von Mustern, UI-Komponenten und Hilfsklassen ausgewählt wurden. Das folgende Beispiel zeigt ein allgemeines Rastersystem, das mit Bootstrap erstellt wurde, das vertikal gestapelt wird und dann, sobald der md-Haltepunkt erreicht ist, zu einem 3-Spalten-Layout wechselt.
<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>
Programmatisch generierte Klassen wie .col-12
und .col-md-4
werden hier verwendet, um dieses Muster zu erstellen. Aber was ist mit .col-1
bis .col-11
, .col-lg-4
, .col-md-6
oder .col-sm-12
? Dies sind alles Beispiele für Klassen, die in das kompilierte CSS-Stylesheet aufgenommen, heruntergeladen und vom Browser geparst werden, obwohl sie nicht verwendet werden.
In diesem Artikel untersuchen wir zunächst die Auswirkungen, die ungenutztes CSS auf die Ladegeschwindigkeit von Seiten haben kann. Wir werden dann eine vorhandene Lösung zum Entfernen aus Stylesheets ansprechen, gefolgt von meiner eigenen Sass-orientierten Lösung.
Messung der Auswirkungen ungenutzter CSS-Klassen
Während ich Sheffield United, die mächtigen Klingen, verehre, ist das CSS ihrer Website in einer einzigen 568-kb-minimierten Datei gebündelt, die selbst mit gzip auf 105 kb kommt. Das scheint viel zu sein.

Sollen wir sehen, wie viel von diesem CSS tatsächlich auf ihrer Homepage verwendet wird? Eine schnelle Google-Suche zeigt viele Online-Tools für den Job, aber ich ziehe es vor, das Coverage -Tool in Chrome zu verwenden, das direkt von den DevTools von Chrome ausgeführt werden kann. Lassen Sie es uns versuchen.

coverage
ein und wählen Sie die Option „Abdeckung anzeigen“. (Große Vorschau)Die Ergebnisse zeigen, dass nur 30 KB CSS des 568 KB großen Stylesheets von der Homepage verwendet werden, wobei die restlichen 538 KB für die Stile stehen, die für den Rest der Website erforderlich sind. Das bedeutet, dass satte 94,8 % des CSS ungenutzt sind.

CSS ist Teil des kritischen Rendering-Pfads einer Webseite, der alle verschiedenen Schritte umfasst, die ein Browser ausführen muss, bevor er mit dem Rendern der Seite beginnen kann. Dies macht CSS zu einem Renderblocker.
In Anbetracht dessen dauert es beim Laden der Website von Sheffield United über eine gute 3G-Verbindung ganze 1,15 Sekunden, bis das CSS heruntergeladen ist und die Seitenwiedergabe beginnen kann. Das ist ein Problem.
Das hat auch Google erkannt. Bei der Durchführung eines Lighthouse-Audits, online oder über Ihren Browser, werden potenzielle Einsparungen bei Ladezeit und Dateigröße hervorgehoben, die durch das Entfernen von nicht verwendetem CSS erzielt werden könnten.

Bestehende Lösungen
Das Ziel besteht darin, festzustellen, welche CSS-Klassen nicht erforderlich sind, und sie aus dem Stylesheet zu entfernen. Existierende Lösungen sind verfügbar, die versuchen, diesen Prozess zu automatisieren. Sie können normalerweise über ein Node.js-Build-Skript oder über Task-Runner wie Gulp verwendet werden. Diese schließen ein:
- UNCSS
- PurifyCSS
- CSS bereinigen
Diese funktionieren im Allgemeinen auf ähnliche Weise:
- Auf Bulld wird die Website über einen Headless-Browser (z. B.: Puppeteer) oder eine DOM-Emulation (z. B.: jsdom) aufgerufen.
- Basierend auf den HTML-Elementen der Seite wird ungenutztes CSS identifiziert.
- Dies wird aus dem Stylesheet entfernt, sodass nur das übrig bleibt, was benötigt wird.
Während diese automatisierten Tools absolut gültig sind und ich viele von ihnen in einer Reihe von kommerziellen Projekten erfolgreich eingesetzt habe, bin ich auf dem Weg dorthin auf einige Nachteile gestoßen, die es wert sind, geteilt zu werden:
- Wenn Klassennamen Sonderzeichen wie „@“ oder „/“ enthalten, werden diese möglicherweise nicht erkannt, ohne benutzerdefinierten Code zu schreiben. Ich verwende BEM-IT von Harry Roberts, bei dem Klassennamen mit responsiven Suffixen strukturiert werden wie:
u-width-6/12@lg
, also bin ich schon einmal auf dieses Problem gestoßen. - Wenn die Website eine automatisierte Bereitstellung verwendet, kann dies den Erstellungsprozess verlangsamen, insbesondere wenn Sie viele Seiten und viel CSS haben.
- Das Wissen über diese Tools muss im gesamten Team geteilt werden, andernfalls kann es zu Verwirrung und Frustration kommen, wenn CSS auf mysteriöse Weise in Produktions-Stylesheets fehlt.
- Wenn auf Ihrer Website viele Skripte von Drittanbietern ausgeführt werden, werden diese manchmal beim Öffnen in einem Headless-Browser nicht gut wiedergegeben und können Fehler beim Filterprozess verursachen. Daher müssen Sie normalerweise benutzerdefinierten Code schreiben, um Skripte von Drittanbietern auszuschließen, wenn ein Headless-Browser erkannt wird, was je nach Konfiguration schwierig sein kann.
- Im Allgemeinen sind diese Arten von Tools kompliziert und bringen viele zusätzliche Abhängigkeiten in den Build-Prozess ein. Wie bei allen Abhängigkeiten von Drittanbietern bedeutet dies, sich auf den Code eines anderen zu verlassen.
Mit diesen Punkten im Hinterkopf habe ich mir eine Frage gestellt:
Ist es mit nur Sass möglich, das Sass, das wir kompilieren, besser zu handhaben, sodass unbenutztes CSS ausgeschlossen werden kann, ohne die Quellklassen im Sass einfach grob zu löschen?
Spoiler-Alarm: Die Antwort ist ja. Hier ist, was ich mir ausgedacht habe.
Sass-orientierte Lösung
Die Lösung muss eine schnelle und einfache Möglichkeit bieten, herauszufinden, was Sass kompiliert werden soll, und gleichzeitig einfach genug sein, um den Entwicklungsprozess nicht noch komplizierter zu machen oder Entwickler daran zu hindern, Vorteile aus Dingen wie programmgesteuertem CSS zu ziehen Klassen.
Für den Anfang gibt es ein Repo mit Build-Skripten und einigen Beispielstilen, die Sie von hier aus klonen können.
Tipp: Wenn Sie nicht weiterkommen, können Sie jederzeit auf die fertige Version im Master-Zweig verweisen.
cd
in das Repo, führen npm install
und dann npm run build
aus, um Sass nach Bedarf in CSS zu kompilieren. Dies sollte eine 55-kb-CSS-Datei im dist-Verzeichnis erstellen.
Wenn Sie dann /dist/index.html
in Ihrem Webbrowser öffnen, sollten Sie eine ziemlich standardmäßige Komponente sehen, die sich beim Klicken erweitert, um einige Inhalte anzuzeigen. Sie können dies auch hier anzeigen, wo reale Netzwerkbedingungen angewendet werden, sodass Sie Ihre eigenen Tests durchführen können.

Filtern auf Partialebene
In einem typischen SCSS-Setup haben Sie wahrscheinlich eine einzelne Manifestdatei (z. B.: main.scss
im Repo) oder eine pro Seite (z. B.: index.scss
, products.scss
, contact.scss
), in der Framework-Partials enthalten sind werden importiert. Gemäß den OOCSS-Prinzipien können diese Importe in etwa so aussehen:
Beispiel 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';
Wenn einer dieser Teilsätze nicht verwendet wird, besteht die natürliche Methode zum Filtern dieses nicht verwendeten CSS darin, den Import einfach zu deaktivieren, wodurch verhindert wird, dass er kompiliert wird.
Wenn Sie beispielsweise nur die Expander-Komponente verwenden, würde das Manifest normalerweise wie folgt aussehen:
Beispiel 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';
Gemäß OOCSS trennen wir jedoch die Dekoration von der Struktur, um eine maximale Wiederverwendbarkeit zu ermöglichen, sodass es möglich ist, dass der Expander CSS von anderen Objekten, Komponenten oder Hilfsklassen benötigt, um korrekt wiedergegeben zu werden. Wenn sich der Entwickler dieser Beziehungen nicht durch Überprüfung des HTML-Codes bewusst ist, weiß er möglicherweise nicht, dass diese Teilkomponenten importiert werden müssen, sodass nicht alle erforderlichen Klassen kompiliert werden.
Wenn Sie sich im Repo den HTML-Code des Expanders in dist/index.html
ansehen, scheint dies der Fall zu sein. Es verwendet Stile aus den Rahmen- und Layoutobjekten, der Typografiekomponente sowie Breiten- und Ausrichtungsdienstprogrammen.
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>
Lassen Sie uns dieses Problem angehen, das darauf wartet, dass es passiert, indem wir diese Beziehungen innerhalb des Sass selbst offiziell machen, sodass sobald eine Komponente importiert wird, auch alle Abhängigkeiten automatisch importiert werden. Auf diese Weise hat der Entwickler nicht länger den zusätzlichen Aufwand, den HTML-Code prüfen zu müssen, um zu erfahren, was er sonst noch importieren muss.
Karte für programmatische Importe
Damit dieses Abhängigkeitssystem funktioniert, muss die Sass-Logik vorschreiben, ob Partials kompiliert werden oder nicht, anstatt einfach @import
Anweisungen in der Manifestdatei zu kommentieren.
Erstellen Sie in src/scss/settings
einen neuen Teil namens _imports.scss
, @import
Sie ihn in settings/_core.scss
und erstellen Sie dann die folgende SCSS-Zuordnung:
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' ) );
Diese Zuordnung hat die gleiche Rolle wie das Importmanifest in Beispiel 1.
Beispiel 4
$imports: ( object: ( //'box', //'container', //'layout' ), component: ( //'button', 'expander', //'typography' ), utility: ( //'alignments', //'widths' ) );
Es sollte sich wie ein Standardsatz von @imports
verhalten, dh wenn bestimmte Teile auskommentiert sind (wie oben), dann sollte dieser Code beim Build nicht kompiliert werden.
Aber da wir Abhängigkeiten automatisch importieren wollen, sollten wir diese Karte unter den richtigen Umständen auch ignorieren können.
Mixin rendern
Beginnen wir damit, etwas Sass-Logik hinzuzufügen. Erstellen Sie _render.scss
in src/scss/tools
und fügen Sie dann @import
zu tools/_core.scss
.
Erstellen Sie in der Datei ein leeres Mixin namens render()
.
src/scss/tools/_render.scss
@mixin render() { }
Im Mixin müssen wir Sass schreiben, was Folgendes tut:
- machen()
„Hallo$imports
, schönes Wetter, nicht wahr? Sagen Sie, haben Sie das Container-Objekt in Ihrer Karte?" - $importe
false
- machen()
„Das ist eine Schande, sieht so aus, als würde es dann nicht kompiliert werden. Wie sieht es mit der Button-Komponente aus?“ - $importe
true
- machen()
"Hübsch! Das ist die Schaltfläche, die dann kompiliert wird. Grüß die Frau von mir.“
In Sass bedeutet dies Folgendes:
src/scss/tools/_render.scss
@mixin render($name, $layer) { @if(index(map-get($imports, $layer), $name)) { @content; } }
Überprüfen Sie grundsätzlich, ob der Partial in der $imports
Variablen enthalten ist, und wenn ja, rendern Sie ihn mit der @content
-Direktive von Sass, die es uns ermöglicht, einen Inhaltsblock an das Mixin zu übergeben.
Wir würden es so verwenden:
Beispiel 5
@include render('button', 'component') { .c-button { // styles et al } // any other class declarations }
Bevor wir dieses Mixin verwenden, gibt es eine kleine Verbesserung, die wir daran vornehmen können. Der Ebenenname (Objekt, Komponente, Dienstprogramm usw.) ist etwas, das wir sicher vorhersagen können, sodass wir die Möglichkeit haben, die Dinge ein wenig zu rationalisieren.
Erstellen Sie vor der Render-Mixin-Deklaration eine Variable namens $layer
und entfernen Sie die gleichnamige Variable aus den Mixins-Parametern. So:
src/scss/tools/_render.scss
$layer: null !default; @mixin render($name) { @if(index(map-get($imports, $layer), $name)) { @content; } }
Deklarieren Sie nun in den _core.scss
Partials, in denen sich Objekte, Komponenten und Dienstprogramm- @imports
befinden, diese Variablen mit den folgenden Werten neu; die den Typ der importierten CSS-Klassen darstellt.
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';
Wenn wir also das render()
-Mixin verwenden, müssen wir nur den partiellen Namen deklarieren.
Wickeln Sie das render()
Mixin wie unten beschrieben um jedes Objekt, jede Komponente und jede Hilfsklassendeklaration. Dadurch erhalten Sie eine Render-Mixin-Nutzung pro Partial.
Zum Beispiel:
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 }
Hinweis: Für utilities/_widths.scss
führt das Umschließen der Funktion render()
um den gesamten Partial zu einem Fehler beim Kompilieren, da Sie in Sass Mixin-Deklarationen nicht in Mixin-Aufrufen verschachteln können. Wickeln Sie stattdessen einfach das render()
Mixin um die create-widths()
-Aufrufe, wie unten:
@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 }
Wenn dies vorhanden ist, werden beim Build nur die Partials kompiliert, auf die in $imports
verwiesen wird.
Mischen und passen Sie an, welche Komponenten in $imports
auskommentiert sind, und führen Sie npm run build
im Terminal aus, um es auszuprobieren.
Abhängigkeitskarte
Jetzt importieren wir programmgesteuert Partials und können mit der Implementierung der Abhängigkeitslogik beginnen.
Erstellen Sie in src/scss/settings
ein neues Teil namens _dependencies.scss
, @import
Sie es in settings/_core.scss
, aber stellen Sie sicher, dass es nach _imports.scss
. Erstellen Sie dann darin die folgende SCSS-Karte:
src/scss/settings/_dependencies.scss
$dependencies: ( expander: ( object: ( 'box', 'layout' ), component: ( 'button', 'typography' ), utility: ( 'alignments', 'widths' ) ) );
Hier deklarieren wir Abhängigkeiten für die Expander-Komponente, da sie Stile von anderen Partials benötigt, um korrekt wiedergegeben zu werden, wie in dist/index.html zu sehen ist.

Mit dieser Liste können wir eine Logik schreiben, die bedeutet, dass diese Abhängigkeiten immer zusammen mit ihren abhängigen Komponenten kompiliert werden, unabhängig vom Status der $imports
Variablen.
Erstellen Sie unterhalb $dependencies
ein Mixin namensdependency dependency-setup()
. Hier führen wir die folgenden Aktionen aus:
1. Durchlaufen Sie die Abhängigkeitskarte.
@mixin dependency-setup() { @each $componentKey, $componentValue in $dependencies { } }
2. Wenn die Komponente in $imports
gefunden werden kann, durchlaufen Sie ihre Liste der Abhängigkeiten.
@mixin dependency-setup() { $components: map-get($imports, component); @each $componentKey, $componentValue in $dependencies { @if(index($components, $componentKey)) { @each $layerKey, $layerValue in $componentValue { } } } }
3. Wenn sich die Abhängigkeit nicht in $imports
befindet, fügen Sie sie hinzu.
@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; } } } } } }
Das Einfügen des !global
-Flags weist Sass an, nach der $imports
Variablen im globalen Geltungsbereich und nicht im lokalen Geltungsbereich des Mixins zu suchen.
4. Dann muss nur noch das Mixin aufgerufen werden.
@mixin dependency-setup() { ... } @include dependency-setup();
Was wir jetzt also haben, ist ein verbessertes partielles Importsystem, bei dem ein Entwickler, wenn eine Komponente importiert wird, nicht auch jede ihrer verschiedenen Teilabhängigkeiten manuell importieren muss.
Konfigurieren Sie die Variable $imports
so, dass nur die Expander-Komponente importiert wird, und führen Sie dann npm run build
. Sie sollten im kompilierten CSS die Expander-Klassen zusammen mit all ihren Abhängigkeiten sehen.
Dies bringt jedoch nicht wirklich etwas Neues in Bezug auf das Herausfiltern von ungenutztem CSS, da immer noch die gleiche Menge an Sass importiert wird, programmatisch oder nicht. Lassen Sie uns das verbessern.
Verbesserter Import von Abhängigkeiten
Eine Komponente benötigt möglicherweise nur eine einzige Klasse aus einer Abhängigkeit. Wenn Sie also fortfahren und alle Klassen dieser Abhängigkeit importieren, führt dies nur zu der gleichen unnötigen Aufblähung, die wir zu vermeiden versuchen.
Wir können das System verfeinern, um eine granularere Filterung auf Klassenbasis zu ermöglichen, um sicherzustellen, dass Komponenten nur mit den Abhängigkeitsklassen kompiliert werden, die sie benötigen.
Bei den meisten Designmustern, dekoriert oder nicht, gibt es eine Mindestanzahl von Klassen, die im Stylesheet vorhanden sein müssen, damit das Muster korrekt angezeigt wird.
Für Klassennamen, die eine etablierte Namenskonvention wie BEM verwenden, sind in der Regel die benannten Klassen „Block“ und „Element“ als Minimum erforderlich, wobei „Modifikatoren“ in der Regel optional sind.
Hinweis: Utility-Klassen würden normalerweise nicht der BEM-Route folgen, da sie aufgrund ihres engen Fokus von Natur aus isoliert sind.
Schauen Sie sich zum Beispiel dieses Medienobjekt an, das wahrscheinlich das bekannteste Beispiel für objektorientiertes CSS ist:
<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>
Wenn eine Komponente diesen Satz als Abhängigkeit hat, ist es sinnvoll, immer .o-media
, .o-media__image
und .o-media__text
zu kompilieren, da dies die Mindestmenge an CSS ist, die erforderlich ist, damit das Muster funktioniert. Da .o-media--spacing-small
jedoch ein optionaler Modifikator ist, sollte es nur kompiliert werden, wenn wir dies ausdrücklich sagen, da seine Verwendung möglicherweise nicht für alle Medienobjektinstanzen konsistent ist.
Wir werden die Struktur der $dependencies
Map ändern, damit wir diese optionalen Klassen importieren können, während wir eine Möglichkeit einschließen, nur den Block und das Element zu importieren, falls keine Modifikatoren erforderlich sind.
Überprüfen Sie zunächst den Expander-HTML in dist/index.html und notieren Sie sich alle verwendeten Abhängigkeitsklassen. Notieren Sie diese wie unten in der $dependencies
Map:
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' ) ) ) );
Wenn ein Wert auf „true“ gesetzt ist, übersetzen wir dies in „Nur Klassen auf Block- und Elementebene kompilieren, keine Modifikatoren!“.
Der nächste Schritt besteht darin, eine Whitelist-Variable zum Speichern dieser Klassen und aller anderen (nicht abhängigen) Klassen zu erstellen, die wir manuell importieren möchten. Erstellen Sie in /src/scss/settings/imports.scss
nach $imports
eine neue Sass-Liste namens $global-filter
.
src/scss/settings/_imports.scss
$global-filter: ();
Die Grundvoraussetzung hinter $global-filter
ist, dass alle hier gespeicherten Klassen beim Build kompiliert werden, solange der Teil, zu dem sie gehören, über $imports
wird.
Diese Klassennamen könnten programmgesteuert hinzugefügt werden, wenn es sich um eine Komponentenabhängigkeit handelt, oder manuell hinzugefügt werden, wenn die Variable deklariert wird, wie im folgenden Beispiel:
Beispiel für einen globalen Filter
$global-filter: ( 'o-box--spacing-regular@md', 'u-align-center', 'u-width-6/12@lg' );
Als Nächstes müssen wir dem @dependency-setup
Mixin etwas mehr Logik hinzufügen, damit alle Klassen, auf die in $dependencies
verwiesen wird, automatisch zu unserer $global-filter
Whitelist hinzugefügt werden.
Unter diesem Block:
src/scss/settings/_dependencies.scss
@if not index(map-get($imports, $layerKey), $partKey) { }
...fügen Sie das folgende Snippet hinzu.
src/scss/settings/_dependencies.scss
@each $class in $partValue { $global-filter: append($global-filter, '#{$class}', 'comma') !global; }
Dadurch werden alle Abhängigkeitsklassen durchlaufen und zur Whitelist von $global-filter
hinzugefügt.
Wenn Sie an dieser Stelle eine @debug
-Anweisung unter dem Mixin „ dependency-setup()
“ hinzufügen, um den Inhalt von „ $global-filter
“ im Terminal auszugeben:
@debug $global-filter;
...sollte beim Build so etwas zu sehen sein:
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"
Jetzt haben wir eine Klassen-Whitelist, die wir für alle verschiedenen Objekt-, Komponenten- und Utility-Partials durchsetzen müssen.
Erstellen Sie ein neues Teil namens _filter.scss
in src/scss/tools
und fügen Sie ein @import
zur Datei _core.scss der Werkzeugebene _core.scss
.
In diesem neuen Partial erstellen wir ein Mixin namens filter()
. Wir verwenden dies, um Logik anzuwenden, was bedeutet, dass Klassen nur kompiliert werden, wenn sie in der Variablen $global-filter
enthalten sind.
Beginnen Sie ganz einfach und erstellen Sie ein Mixin, das einen einzigen Parameter akzeptiert – die $class
, die der Filter steuert. Wenn die $class
in der Whitelist $global-filter
enthalten ist, lassen Sie als Nächstes zu, dass sie kompiliert wird.
src/scss/tools/_filter.scss
@mixin filter($class) { @if(index($global-filter, $class)) { @content; } }
In einem Partial würden wir das Mixin wie folgt um eine optionale Klasse wickeln:
@include filter('o-myobject--modifier') { .o-myobject--modifier { color: yellow; } }
Das bedeutet, dass die Klasse .o-myobject--modifier
nur kompiliert wird, wenn sie in $global-filter
enthalten ist, was entweder direkt oder indirekt über die in $dependencies
gesetzten Werte gesetzt werden kann.
Gehen Sie durch das Repo und wenden Sie das filter()
Mixin auf alle optionalen Modifikatorklassen über Objekt- und Komponentenebenen hinweg an. Da jede Klasse unabhängig von der nächsten ist, wäre es beim Umgang mit der Typografiekomponente oder der Utilities-Schicht sinnvoll, sie alle optional zu machen, damit wir dann einfach Klassen aktivieren können, wenn wir sie brauchen.
Hier ein paar Beispiele:
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; } } } }
Hinweis: Wenn Sie dem filter()
-Mixin die responsiven Suffix-Klassennamen hinzufügen, müssen Sie das „@“-Symbol nicht mit einem „\“ maskieren.
Während dieses Prozesses, während Sie das filter()
Mixin auf Teiltöne anwenden, haben Sie vielleicht (oder auch nicht) ein paar Dinge bemerkt.
Gruppierte Klassen
Einige Klassen in der Codebasis sind gruppiert und haben dieselben Stile, zum Beispiel:
src/scss/objects/_box.scss
.o-box--spacing-disable-left, .o-box--spacing-horizontal { padding-left: 0; }
Da der Filter nur eine einzige Klasse akzeptiert, berücksichtigt er nicht die Möglichkeit, dass ein Stildeklarationsblock für mehr als eine Klasse gilt.
Um dies zu berücksichtigen, erweitern wir das filter()
Mixin so, dass es zusätzlich zu einer einzelnen Klasse eine Sass-Arglist akzeptieren kann, die viele Klassen enthält. So:
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; } }
Wir müssen also dem mixin filter()
mitteilen, dass Sie die Klassen kompilieren dürfen, wenn sich eine dieser Klassen im $global-filter
befindet.
Dies erfordert zusätzliche Logik, um das $class
Argument des Mixins zu überprüfen und mit einer Schleife zu antworten, wenn eine Argumentliste übergeben wird, um zu prüfen, ob sich jedes Element in der $global-filter
Variablen befindet.
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; } }
Dann müssen Sie nur noch zu den folgenden Teiltönen zurückkehren, um das filter()
-Mixin korrekt anzuwenden:
-
objects/_box.scss
-
objects/_layout.scss
-
utilities/_alignments.scss
Gehen Sie an dieser Stelle zurück zu $imports
und aktivieren Sie nur die Expander-Komponente. Im kompilierten Stylesheet sollten Sie neben den Stilen aus den Ebenen Generic und Elements nur Folgendes sehen:
- Die Block- und Elementklassen, die zur Expander-Komponente gehören, aber nicht ihr Modifikator.
- Die Block- und Elementklassen, die zu den Abhängigkeiten des Expanders gehören.
- Alle Modifikatorklassen, die zu den Abhängigkeiten des Expanders gehören und explizit in der
$dependencies
Variablen deklariert sind.
Wenn Sie sich entschieden haben, mehr Klassen in das kompilierte Stylesheet aufzunehmen, wie zum Beispiel den Expander-Komponenten-Modifikator, müssen Sie ihn theoretisch nur zum Zeitpunkt der Deklaration zur Variablen $global-filter
hinzufügen oder an einem anderen Punkt anhängen in der Codebasis (solange es vor dem Punkt steht, an dem der Modifikator selbst deklariert wird).
Alles aktivieren
Wir haben jetzt also ein ziemlich vollständiges System, mit dem Sie Objekte, Komponenten und Dienstprogramme bis hinunter zu den einzelnen Klassen innerhalb dieser Partials importieren können.
Während der Entwicklung möchten Sie aus irgendeinem Grund vielleicht einfach alles auf einmal aktivieren. Um dies zu ermöglichen, erstellen wir eine neue Variable namens $enable-all-classes
und fügen dann zusätzliche Logik hinzu. Wenn dies also auf true gesetzt ist, wird alles kompiliert, unabhängig vom Status der $imports
und $global-filter
Variablen $global-filter
.
Deklarieren Sie zuerst die Variable in unserer Hauptmanifestdatei:
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';
Dann müssen wir nur noch ein paar kleinere Änderungen an unseren filter()
und render()
Mixins vornehmen, um eine Override-Logik hinzuzufügen, wenn die Variable $enable-all-classes
auf true gesetzt ist.
Zuerst das filter()
Mixin. Vor allen vorhandenen Prüfungen fügen wir eine @if
hinzu, um zu sehen, ob $enable-all-classes
auf true gesetzt ist, und wenn ja, rendern wir den @content
, ohne dass Fragen gestellt werden.
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; } }
Als Nächstes müssen wir im render()
-Mixin nur prüfen, ob die Variable $enable-all-classes
wahr ist, und wenn ja, alle weiteren Prüfungen überspringen.
src/scss/tools/_render.scss
$layer: null !default; @mixin render($name) { @if($enable-all-classes or index(map-get($imports, $layer), $name)) { @content; } }
Wenn Sie also jetzt die Variable $enable-all-classes
auf true setzen und neu erstellen, würde jede optionale Klasse kompiliert, was Ihnen eine Menge Zeit in diesem Prozess spart.
Vergleiche
Um zu sehen, welche Art von Gewinnen uns diese Technik bringt, lassen Sie uns einige Vergleiche durchführen und sehen, was die Dateigrößenunterschiede sind.
Um sicherzustellen, dass der Vergleich fair ist, sollten wir die Box- und Container-Objekte in $imports
hinzufügen und dann den Modifikator o-box--spacing-regular
der Box zum $global-filter
hinzufügen, etwa so:
src/scss/settings/_imports.scss
$imports: ( object: ( 'box', 'container' // 'layout' ), component: ( // 'button', 'expander' // 'typography' ), utility: ( // 'alignments', // 'widths' ) ); $global-filter: ( 'o-box--spacing-regular' );
Dadurch wird sichergestellt, dass Stile für die übergeordneten Elemente des Expanders so kompiliert werden, als ob keine Filterung stattfinden würde.
Ursprüngliche vs. gefilterte Stylesheets
Vergleichen wir das ursprüngliche Stylesheet mit allen kompilierten Klassen mit dem gefilterten Stylesheet, in dem nur CSS kompiliert wurde, das von der Expander-Komponente benötigt wird.
Standard | ||
---|---|---|
Stylesheet | Größe (KB) | Größe (gzip) |
Original | 54,6 KB | 6,98 KB |
Gefiltert | 15,34 kb (72 % kleiner) | 4,91 kb (29 % kleiner) |
- Original: https://webdevluke.github.io/handlingunusedcss/dist/index2.html
- Gefiltert: https://webdevluke.github.io/handlingunusedcss/dist/index.html
Sie denken vielleicht, dass die prozentuale Einsparung von gzip bedeutet, dass sich die Mühe nicht lohnt, da es keinen großen Unterschied zwischen den ursprünglichen und gefilterten Stylesheets gibt.
Hervorzuheben ist, dass die gzip-Komprimierung bei größeren und sich wiederholenden Dateien besser funktioniert. Da das gefilterte Stylesheet der einzige Proof-of-Concept ist und nur CSS für die Expander-Komponente enthält, muss nicht so viel komprimiert werden wie in einem realen Projekt.
Wenn wir jedes Stylesheet um den Faktor 10 auf Größen skalieren würden, die eher für die CSS-Bundle-Größe einer Website typisch sind, wäre der Unterschied in der gzip-Dateigröße viel beeindruckender.
10x Größe | ||
---|---|---|
Stylesheet | Größe (KB) | Größe (gzip) |
Original (10x) | 892,07 KB | 75,70 KB |
Gefiltert (10x) | 209,45 kb (77 % kleiner) | 19,47 kb (74 % kleiner) |
Gefiltertes Stylesheet vs. UNCSS
Hier ist ein Vergleich zwischen dem gefilterten Stylesheet und einem Stylesheet, das das UNCSS-Tool durchlaufen hat.
Gefiltert vs. UNCSS | ||
---|---|---|
Stylesheet | Größe (KB) | Größe (gzip) |
Gefiltert | 15,34 KB | 4,91 KB |
UNCSS | 12,89 kb (16 % kleiner) | 4,25 kb (13 % kleiner) |
Das UNCSS-Tool gewinnt hier knapp, da es CSS in den Verzeichnissen Generic und Elements herausfiltert.
Es ist möglich, dass auf einer echten Website mit einer größeren Vielfalt an verwendeten HTML-Elementen der Unterschied zwischen den beiden Methoden vernachlässigbar wäre.
Einpacken
Wir haben also gesehen, wie Sie – wenn Sie nur Sass verwenden – mehr Kontrolle darüber erlangen können, welche CSS-Klassen beim Build kompiliert werden. Dies reduziert die Menge an ungenutztem CSS im endgültigen Stylesheet und beschleunigt den kritischen Rendering-Pfad.
Zu Beginn des Artikels habe ich einige Nachteile bestehender Lösungen wie UNCSS aufgelistet. Es ist nur fair, diese Sass-orientierte Lösung auf die gleiche Weise zu kritisieren, damit alle Fakten auf dem Tisch liegen, bevor Sie entscheiden, welcher Ansatz für Sie besser ist:
Vorteile
- Keine zusätzlichen Abhängigkeiten erforderlich, sodass Sie sich nicht auf den Code einer anderen Person verlassen müssen.
- Weniger Build-Zeit erforderlich als Node.js-basierte Alternativen, da Sie keine Headless-Browser ausführen müssen, um Ihren Code zu prüfen. Dies ist besonders nützlich bei kontinuierlicher Integration, da Sie möglicherweise weniger wahrscheinlich eine Warteschlange von Builds sehen.
- Führt im Vergleich zu automatisierten Tools zu einer ähnlichen Dateigröße.
- Sie haben sofort die vollständige Kontrolle darüber, welcher Code gefiltert wird, unabhängig davon, wie diese CSS-Klassen in Ihrem Code verwendet werden. Bei Node.js-basierten Alternativen müssen Sie oft eine separate Whitelist pflegen, damit CSS-Klassen, die zu dynamisch injiziertem HTML gehören, nicht herausgefiltert werden.
Nachteile
- Die Sass-orientierte Lösung ist definitiv praktischer, in dem Sinne, dass Sie die
$imports
und$global-filter
Variablen im Auge behalten müssen. Abgesehen von der anfänglichen Einrichtung sind die von uns betrachteten Node.js-Alternativen weitgehend automatisiert. - Wenn Sie CSS-Klassen zu
$global-filter
hinzufügen und sie später aus Ihrem HTML entfernen, müssen Sie daran denken, die Variable zu aktualisieren, da Sie sonst CSS kompilieren, das Sie nicht benötigen. Bei großen Projekten, an denen mehrere Entwickler gleichzeitig arbeiten, ist dies möglicherweise nicht einfach zu verwalten, es sei denn, Sie planen dies richtig. - Ich würde nicht empfehlen, dieses System auf eine vorhandene CSS-Codebasis zu schrauben, da Sie ziemlich viel Zeit damit verbringen müssten, Abhängigkeiten zusammenzusetzen und das
render()
Mixin auf eine MENGE Klassen anzuwenden. Es ist ein System, das viel einfacher mit neuen Builds zu implementieren ist, wo Sie sich nicht mit vorhandenem Code auseinandersetzen müssen.
Hoffentlich fanden Sie das Lesen so interessant, wie ich es interessant fand, es zusammenzustellen. Wenn Sie Vorschläge oder Ideen zur Verbesserung dieses Ansatzes haben oder auf einen schwerwiegenden Fehler hinweisen möchten, den ich völlig übersehen habe, posten Sie ihn unbedingt in den Kommentaren unten.