Umgang mit nicht verwendetem CSS in Sass zur Verbesserung der Leistung

Veröffentlicht: 2022-03-10
Kurze Zusammenfassung ↬ Kennen Sie die Auswirkungen von nicht verwendetem CSS auf die Leistung? Spoiler: Es ist viel! In diesem Artikel untersuchen wir eine Sass-orientierte Lösung für den Umgang mit nicht verwendetem CSS, wodurch die Notwendigkeit komplizierter Node.js-Abhängigkeiten mit Headless-Browsern und DOM-Emulation vermieden wird.

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

Mehr nach dem Sprung! Lesen Sie unten weiter ↓

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.

Dies ist die Website von Sheffield United, meiner lokalen Fußballmannschaft (das ist Fußball für Sie in den Kolonien). (Große Vorschau)

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.

Der schnellste Weg, auf das Abdeckungstool in den Entwicklertools zuzugreifen, ist die Verwendung der Tastenkombination Strg+Umschalt+P oder Befehl+Umschalt+P (Mac), um das Befehlsmenü zu öffnen. Geben Sie dort 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.

Sie können Timings wie diese für jedes Asset in Chrome in den Entwicklertools über Netzwerk -> Klicken Sie auf Ihr Asset -> Registerkarte Timing sehen. (Große Vorschau)

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.

In Chrome (und Chromium Edge) können Sie Google Lighthouse-Audits korrigieren, indem Sie in den Entwicklertools auf die Registerkarte Audit klicken. (Große Vorschau)

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:

  1. Auf Bulld wird die Website über einen Headless-Browser (z. B.: Puppeteer) oder eine DOM-Emulation (z. B.: jsdom) aufgerufen.
  2. Basierend auf den HTML-Elementen der Seite wird ungenutztes CSS identifiziert.
  3. 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.

Wir werden diese Expander-UI-Komponente als unser Testobjekt verwenden, wenn wir die Sass-orientierte Lösung für den Umgang mit nicht verwendetem CSS entwickeln. (Große Vorschau)

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.