Eine gründliche Analyse von CSS-in-JS

Avatar of Andrei Pfeiffer
Andrei Pfeiffer am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Fragen Sie sich, was noch schwieriger ist als die Wahl eines JavaScript-Frameworks? Sie haben es erraten: die Wahl einer CSS-in-JS-Lösung. Warum? Weil es mehr als 50 Bibliotheken gibt, die jeweils einen einzigartigen Satz von Funktionen bieten.

Wir haben 10 verschiedene Bibliotheken getestet, die hier in keiner bestimmten Reihenfolge aufgeführt sind: Styled JSX, styled-components, Emotion, Treat, TypeStyle, Fela, Stitches, JSS, Goober und Compiled. Wir stellten fest, dass, obwohl jede Bibliothek eine vielfältige Palette von Funktionen bietet, viele dieser Funktionen tatsächlich zwischen den meisten anderen Bibliotheken gemeinsam sind.

Anstatt also jede Bibliothek einzeln zu rezensieren, werden wir die Funktionen analysieren, die am meisten hervorstechen. Dies wird uns helfen, besser zu verstehen, welche am besten für einen bestimmten Anwendungsfall geeignet ist.

Hinweis: Wir gehen davon aus, dass Sie, wenn Sie hier sind, bereits mit CSS-in-JS vertraut sind. Wenn Sie einen grundlegenderen Beitrag suchen, können Sie sich „Eine Einführung in CSS-in-JS“ ansehen.

Gemeinsame CSS-in-JS-Funktionen

Die meisten aktiv gepflegten Bibliotheken, die sich mit CSS-in-JS befassen, unterstützen alle folgenden Funktionen, sodass wir sie als de facto betrachten können.

Gekapselte CSS

Alle CSS-in-JS-Bibliotheken generieren eindeutige CSS-Klassennamen, eine Technik, die von CSS Modules eingeführt wurde. Alle Stile sind auf ihre jeweilige Komponente beschränkt und bieten Kapselung, ohne die außerhalb der Komponente definierten Stile zu beeinträchtigen.

Mit dieser integrierten Funktion müssen wir uns nie Gedanken über Kollisionen von CSS-Klassennamen, Spezifitätskriege oder verschwendete Zeit bei der Suche nach eindeutigen Klassennamen im gesamten Codebase machen.

Diese Funktion ist für die komponentenbasierte Entwicklung von unschätzbarem Wert.

SSR (Server-Side Rendering)

Wenn wir Single Page Apps (SPAs) betrachten – bei denen der HTTP-Server nur eine anfängliche leere HTML-Seite liefert und die gesamte Darstellung im Browser erfolgt –, ist Server-Side Rendering (SSR) möglicherweise nicht sehr nützlich. Jede Website oder Anwendung, die von Suchmaschinen analysiert und indiziert werden muss, benötigt jedoch SSR-Seiten, und auch die Stile müssen auf dem Server generiert werden.

Das gleiche Prinzip gilt für Static Site Generators (SSG), bei denen Seiten zusammen mit jedem CSS-Code zur Build-Zeit als statische HTML-Dateien vorab generiert werden.

Die gute Nachricht ist, dass alle von uns getesteten Bibliotheken SSR unterstützen und sie damit für praktisch jeden Projekttyp in Frage kommen.

Automatische Vendor-Präfixe

Aufgrund des komplexen CSS-Standardisierungsprozesses kann es Jahre dauern, bis eine neue CSS-Funktion in allen gängigen Browsern verfügbar ist. Ein Ansatz zur Bereitstellung eines frühen Zugangs zu experimentellen Funktionen ist die Bereitstellung nicht standardmäßiger CSS-Syntax unter einem Vendor-Präfix

/* WebKit browsers: Chrome, Safari, most iOS browsers, etc */
-webkit-transition: all 1s ease;

/* Firefox */
-moz-transition: all 1s ease;

/* Internet Explorer and Microsoft Edge */
-ms-transition: all 1s ease;

/* old pre-WebKit versions of Opera */
-o-transition: all 1s ease;

/* standard */
transition: all 1s ease; 

Es stellt sich jedoch heraus, dass Vendor-Präfixe problematisch sind und die CSS Working Group beabsichtigt, sie in Zukunft nicht mehr zu verwenden. Wenn wir ältere Browser, die die Standard-Spezifikation nicht implementieren, vollständig unterstützen wollen, müssen wir wissen, welche Funktionen ein Vendor-Präfix erfordern.

Glücklicherweise gibt es Tools, die es uns ermöglichen, die Standard-Syntax in unserem Quellcode zu verwenden und die erforderlichen Vendor-Präfix-CSS-Eigenschaften automatisch zu generieren. Alle CSS-in-JS-Bibliotheken bieten diese Funktion ebenfalls standardmäßig an.

Keine Inline-Stile

Es gibt einige CSS-in-JS-Bibliotheken, wie Radium oder Glamor, die alle Stildefinitionen als Inline-Stile ausgeben. Diese Technik hat eine erhebliche Einschränkung, da es unmöglich ist, Pseudo-Klassen, Pseudo-Elemente oder Media Queries mit Inline-Stilen zu definieren. Daher mussten diese Bibliotheken diese Funktionen durch Hinzufügen von DOM-Event-Listenern hacken und Stilaktualisierungen von JavaScript aus auslösen, im Wesentlichen native CSS-Funktionen wie :hover, :focus und viele mehr neu erfinden.

Es ist auch allgemein anerkannt, dass Inline-Stile weniger performant sind als Klassennamen. Es ist üblicherweise eine abgeratene Praxis, sie als primären Ansatz für das Styling von Komponenten zu verwenden.

Alle aktuellen CSS-in-JS-Bibliotheken haben sich von der Verwendung von Inline-Stilen abgewandt und CSS-Klassennamen übernommen, um Stildefinitionen anzuwenden.

Volle CSS-Unterstützung

Eine Folge der Verwendung von CSS-Klassen anstelle von Inline-Stilen ist, dass es keine Einschränkungen gibt, welche CSS-Eigenschaften wir verwenden können und welche nicht. Während unserer Analyse waren wir besonders interessiert an

  • Pseudo-Klassen und -Elementen;
  • Media Queries;
  • Keyframe-Animationen.

Alle von uns analysierten Bibliotheken bieten volle Unterstützung für alle CSS-Eigenschaften.

Unterscheidende Merkmale

Hier wird es noch interessanter. Fast jede Bibliothek bietet eine einzigartige Reihe von Funktionen, die unsere Entscheidung bei der Auswahl der geeigneten Lösung für ein bestimmtes Projekt stark beeinflussen können. Einige Bibliotheken haben eine bestimmte Funktion eingeführt, während andere bestimmte Funktionen übernommen oder sogar verbessert haben.

React-spezifisch oder Framework-agnostisch?

Es ist kein Geheimnis, dass CSS-in-JS im React-Ökosystem weiter verbreitet ist. Deshalb sind einige Bibliotheken speziell für React entwickelt worden: Styled JSX, styled-components und Stitches.

Es gibt jedoch viele Bibliotheken, die Framework-agnostisch sind und daher für jedes Projekt verwendet werden können: Emotion, Treat, TypeStyle, Fela, JSS oder Goober.

Wenn wir Vanilla-JavaScript-Code oder andere Frameworks als React unterstützen müssen, ist die Entscheidung einfach: Wir sollten eine Framework-agnostische Bibliothek wählen. Wenn wir uns jedoch mit einer React-Anwendung befassen, haben wir eine viel größere Auswahl, was die Entscheidung letztendlich schwieriger macht. Lassen Sie uns also andere Kriterien untersuchen.

Co-Location von Stilen/Komponenten

Die Möglichkeit, Stile zusammen mit ihren Komponenten zu definieren, ist eine sehr praktische Funktion, die die Notwendigkeit beseitigt, zwischen zwei verschiedenen Dateien hin und her zu wechseln: der .css- oder .less/.scss-Datei, die die Stile enthält, und der Komponentendatei, die das Markup und Verhalten enthält.

React Native StyleSheets, Vue.js SFCs oder Angular Components unterstützen die Co-Location von Stilen standardmäßig, was sich sowohl in der Entwicklung als auch in der Wartung als echter Vorteil erweist. Wir haben immer die Möglichkeit, die Stile in eine separate Datei zu extrahieren, falls wir der Meinung sind, dass sie den Rest des Codes verdecken.

Fast alle CSS-in-JS-Bibliotheken unterstützen die Co-Location von Stilen. Die einzige Ausnahme, auf die wir gestoßen sind, war Treat, die verlangt, dass wir die Stile in einer separaten .treat.ts-Datei definieren, ähnlich wie bei CSS Modules.

Syntax der Stildefinition

Es gibt zwei verschiedene Methoden, mit denen wir unsere Stile definieren können. Einige Bibliotheken unterstützen nur eine Methode, während andere recht flexibel sind und beide unterstützen.

Tagged Templates-Syntax

Die Tagged Templates-Syntax erlaubt es uns, Stile als eine Zeichenkette aus einfachem CSS-Code innerhalb eines Standard- ES-Template-Literal zu definieren

// consider "css" being the API of a generic CSS-in-JS library
const heading = css`
  font-size: 2em;
  color: ${myTheme.color};
`;

Wir sehen, dass

  • CSS-Eigenschaften in Kebab-Case geschrieben sind, genau wie reguläres CSS;
  • JavaScript-Werte interpoliert werden können;
  • wir bestehenden CSS-Code einfach migrieren können, ohne ihn neu schreiben zu müssen.

Einige Dinge, die man beachten sollte

  • Um Syntaxhervorhebung und Code-Vervollständigung zu erhalten, ist ein zusätzliches Editor-Plugin erforderlich; dieses Plugin ist jedoch normalerweise für gängige Editoren wie VSCode, WebStorm und andere verfügbar.
  • Da der endgültige Code letztendlich in JavaScript ausgeführt werden muss, müssen die Stildefinitionen analysiert und in JavaScript-Code konvertiert werden. Dies kann entweder zur Laufzeit oder zur Build-Zeit erfolgen und verursacht einen geringen Overhead bei der Bundle-Größe oder der Berechnung.
Object Styles-Syntax

Die Object Styles-Syntax erlaubt es uns, Stile als reguläre JavaScript-Objekte zu definieren

// consider "css" being the API of a generic CSS-in-JS library
const heading = css({
  fontSize: "2em",
  color: myTheme.color,
});

Wir sehen, dass

  • CSS-Eigenschaften sind in CamelCase geschrieben und Zeichenfolgenwerte müssen in Anführungszeichen gesetzt werden;
  • JavaScript-Werte können wie erwartet referenziert werden;
  • es fühlt sich nicht wie das Schreiben von CSS an, da wir stattdessen Stile mit einer leicht unterschiedlichen Syntax definieren, aber mit den gleichen Eigenschaftsnamen und -werten wie in CSS (lassen Sie sich davon nicht einschüchtern, Sie werden sich schnell daran gewöhnen);
  • die Migration bestehender CSS würde eine Umschreibung in dieser neuen Syntax erfordern.

Einige Dinge, die man beachten sollte

  • Syntaxhervorhebung ist standardmäßig verfügbar, da wir tatsächlich JavaScript-Code schreiben.
  • Um Code-Vervollständigung zu erhalten, muss die Bibliothek CSS-Typdefinitionen bereitstellen, von denen die meisten das beliebte CSSType-Paket erweitern.
  • Da die Stile bereits in JavaScript geschrieben sind, ist keine zusätzliche Parsing- oder Konvertierung erforderlich.
BibliothekTagged templateObject styles
styled-components
Emotion
Goober
Compiled
Fela🟠
JSS🟠
Treat
TypeStyle
Stitches
Styled JSX

✅  Volle Unterstützung         🟠  Benötigt Plugin          ❌  Nicht unterstützt

Methode zum Anwenden von Stilen

Nachdem wir nun wissen, welche Optionen für die Stildefinition verfügbar sind, schauen wir uns an, wie sie auf unsere Komponenten und Elemente angewendet werden.

Verwendung eines Klassenattributs / className-Props

Der einfachste und intuitivste Weg, die Stile anzuwenden, ist, sie einfach als Klassennamen anzuhängen. Bibliotheken, die diesen Ansatz unterstützen, bieten eine API, die eine Zeichenkette zurückgibt, die die generierten eindeutigen Klassennamen ausgibt

// consider "css" being the API of a generic CSS-in-JS library
const heading_style = css({
  color: "blue"
});

Als Nächstes können wir heading_style, das eine Zeichenkette von generierten CSS-Klassennamen enthält, nehmen und sie auf unser HTML-Element anwenden

// Vanilla DOM usage
const heading = `<h1 class="${heading_style}">Title</h1>`;

// React-specific JSX usage
function Heading() {
  return <h1 className={heading_style}>Title</h1>;
}

Wie wir sehen, ähnelt diese Methode ziemlich dem traditionellen Styling: Zuerst definieren wir die Stile, dann hängen wir die Stile dort an, wo wir sie brauchen. Die Lernkurve ist niedrig für jeden, der bereits CSS geschrieben hat.

Verwendung einer <Styled />-Komponente

Eine weitere beliebte Methode, die zuerst von der styled-components-Bibliothek eingeführt wurde (und nach ihr benannt ist), verfolgt einen anderen Ansatz.

// consider "styled" being the API for a generic CSS-in-JS library
const Heading = styled("h1")({
  color: "blue"
});

Anstatt die Stile separat zu definieren und sie an bestehende Komponenten oder HTML-Elemente anzuhängen, verwenden wir eine spezielle API, indem wir angeben, welche Art von Element wir erstellen möchten und welche Stile wir daran anhängen möchten.

Die API gibt eine neue Komponente zurück, mit bereits angewendeten Klassennamen, die wir wie jede andere Komponente in unserer Anwendung rendern können. Dies beseitigt im Grunde die Zuordnung zwischen der Komponente und ihren Stilen.

Verwendung des css-Props

Eine neuere Methode, die von Emotion populär gemacht wurde, erlaubt es uns, die Stile an ein spezielles Prop zu übergeben, das normalerweise css genannt wird. Diese API ist nur für JSX-basierte Syntax verfügbar.

// React-specific JSX syntax
function Heading() {
  return <h1 css={{ color: "blue" }}>Title</h1>;
}

Dieser Ansatz hat einen gewissen ergonomischen Vorteil, da wir keine spezielle API aus der Bibliothek importieren und verwenden müssen. Wir können die Stile einfach an dieses css-Prop übergeben, ähnlich wie wir Inline-Stile verwenden würden.

Beachten Sie, dass dieses benutzerdefinierte css-Prop kein Standard-HTML-Attribut ist und über ein separates Babel-Plugin, das von der Bibliothek bereitgestellt wird, aktiviert und unterstützt werden muss.

BibliothekclassName<Styled />css Prop
styled-components
Emotion
Goober🟠 2
Compiled🟠 1
Fela
JSS🟠 2
Treat
TypeStyle
Stitches🟠 1
Styled JSX

✅  Volle Unterstützung          🟠 1  Begrenzte Unterstützung          🟠 2  Benötigt Plugin          ❌  Nicht unterstützt

Stil-Ausgabe

Es gibt zwei sich gegenseitig ausschließende Methoden, um Stile zu generieren und an den Browser zu liefern. Beide Methoden haben Vor- und Nachteile, analysieren wir sie also im Detail.

<style>-injektierte DOM-Stile

Die meisten CSS-in-JS-Bibliotheken injizieren Stile zur Laufzeit in das DOM, entweder über einen oder mehrere <style>-Tags oder über die CSSStyleSheet-API, um Stile direkt im CSSOM zu verwalten. Während SSR werden Stile immer als <style>-Tag im <head> der gerenderten HTML-Seite angehängt.

Es gibt einige wichtige Vorteile und bevorzugte Anwendungsfälle für diesen Ansatz

  1. Das Inlining der Stile während SSR bietet eine Steigerung der Seitenladeleistungsmetriken wie FCP (First Contentful Paint), da das Rendering nicht durch das Abrufen einer separaten .css-Datei vom Server blockiert wird.
  2. Es bietet standardmäßig Critical CSS Extraction während SSR, indem nur die für das Rendern der anfänglichen HTML-Seite erforderlichen Stile inline gesetzt werden. Außerdem werden alle dynamischen Stile entfernt, was die Ladezeit weiter verbessert, da weniger Code heruntergeladen wird.
  3. Dynamisches Styling ist in der Regel einfacher zu implementieren, da dieser Ansatz besser für hochgradig interaktive Benutzeroberflächen und Single-Page Applications (SPA) geeignet zu sein scheint, bei denen die meisten Komponenten clientseitig gerendert werden.

Die Nachteile sind im Allgemeinen mit der Gesamtgröße des Bundles verbunden

  • eine zusätzliche Laufzeitbibliothek wird für die Handhabung des dynamischen Stylings im Browser benötigt;
  • die Inline-SSR-Stile werden nicht standardmäßig zwischengespeichert und müssen bei jeder Anfrage an den Browser gesendet werden, da sie Teil der vom Server gerenderten .html-Datei sind;
  • die SSR-Stile, die in die .html-Datei inline gesetzt werden, werden während des Rehydration-Prozesses erneut als JavaScript-Ressourcen an den Browser gesendet.
Statische .css-Dateiexktion

Es gibt eine sehr kleine Anzahl von Bibliotheken, die einen völlig anderen Ansatz verfolgen. Anstatt die Stile in das DOM zu injizieren, generieren sie statische .css-Dateien. Aus Sicht der Ladeleistung erhalten wir die gleichen Vor- und Nachteile wie beim Schreiben von reinem CSS.

  1. Die Gesamtmenge des ausgelieferten Codes ist viel geringer, da kein zusätzlicher Laufzeitcode oder Rehydration-Overhead erforderlich ist.
  2. Statische .css-Dateien profitieren von der Browser-Caching-Funktion, sodass nachfolgende Anfragen an dieselbe Seite die Stile nicht erneut abrufen müssen.
  3. Dieser Ansatz scheint attraktiver zu sein, wenn es um SSR-Seiten oder statisch generierte Seiten geht, da diese von den Standard-Caching-Mechanismen profitieren.

Es gibt jedoch einige wichtige Nachteile, die wir beachten müssen

Fast alle von uns getesteten Bibliotheken implementieren die erste Methode, bei der die Stile in das DOM injiziert werden. Die einzige getestete Bibliothek, die statische .css-Dateien extrahiert, ist Treat. Es gibt andere Bibliotheken, die diese Funktion unterstützen, wie Astroturf, Linaria und style9, die nicht in unsere endgültige Analyse aufgenommen wurden.

Atomic CSS

Einige Bibliotheken gingen bei der Optimierung noch einen Schritt weiter und implementierten eine Technik namens Atomic CSS-in-JS, inspiriert von Frameworks wie Tachyons oder Tailwind.

Anstatt CSS-Klassen zu generieren, die alle für ein bestimmtes Element definierten Eigenschaften enthalten, generieren sie für jede eindeutige CSS-Eigenschaft/-Wert-Kombination eine eindeutige CSS-Klasse.

/* classic, non-atomic CSS class */
._wqdGktJ {
  color: blue;
  display: block;
  padding: 1em 2em;
}

/* atomic CSS classes */
._ktJqdG { color: blue; }
._garIHZ { display: block; }
/* short-hand properties are usually expanded */
._kZbibd { padding-right: 2em; }
._jcgYzk { padding-left: 2em; }
._ekAjen { padding-bottom: 1em; }
._ibmHGN { padding-top: 1em; }

Dies ermöglicht ein hohes Maß an Wiederverwendbarkeit, da jede dieser einzelnen CSS-Klassen überall in der Codebasis wiederverwendet werden kann.

Theoretisch funktioniert dies in großen Anwendungen wirklich gut. Warum? Weil es eine endliche Anzahl von eindeutigen CSS-Eigenschaften gibt, die für eine gesamte Anwendung benötigt werden. Der Skalierungsfaktor ist daher nicht linear, sondern logarithmisch, was zu weniger CSS-Ausgabe im Vergleich zu nicht-atomarem CSS führt.

Aber es gibt einen Haken: individuelle Klassennamen müssen jedem Element zugewiesen werden, das sie benötigt, was zu etwas größeren HTML-Dateien führt.

<!-- with classic, non-atomic CSS classes -->
<h1 class="_wqdGktJ">...</h1>

<!-- with atomic CSS classes -->
<h1 class="_ktJqdG _garIHZ _kZbibd _jcgYzk _ekAjen _ibmHGN">...</h1>

Wir verlagern den Code also im Grunde von CSS nach HTML. Die resultierende Größenänderung hängt von zu vielen Aspekten ab, als dass wir eine definitive Schlussfolgerung ziehen könnten, aber generell sollte die Gesamtmenge der an den Browser gelieferten Bytes reduziert werden.

Fazit

CSS-in-JS wird die Art und Weise, wie wir CSS erstellen, dramatisch verändern und viele Vorteile bieten sowie unsere allgemeine Entwicklungserfahrung verbessern.

Die Wahl der zu übernehmenden Bibliothek ist jedoch nicht einfach und alle Entscheidungen gehen mit vielen technischen Kompromissen einher. Um die für unsere Bedürfnisse am besten geeignete Bibliothek zu identifizieren, müssen wir die Projektanforderungen und ihre Anwendungsfälle verstehen.

  • Verwenden wir React oder nicht? React-Anwendungen haben eine größere Auswahl, während Nicht-React-Lösungen eine Framework-agnostische Bibliothek verwenden müssen.
  • Beschäftigen wir uns mit einer hochgradig interaktiven Anwendung mit clientseitiger Rendern? In diesem Fall sind wir wahrscheinlich nicht sehr besorgt über den Overhead der Rehydrierung oder kümmern uns viel um die Extraktion statischer .css-Dateien.
  • Bauen wir eine dynamische Website mit SSR-Seiten? Dann ist die Extraktion statischer .css-Dateien wahrscheinlich eine bessere Option, da sie uns die Vorteile des Cachings ermöglicht.
  • Müssen wir vorhandenen CSS-Code migrieren? Die Verwendung einer Bibliothek, die Tagged Templates unterstützt, erleichtert und beschleunigt die Migration.
  • Wollen wir für Erstbesucher oder wiederkehrende Besucher optimieren? Statische .css-Dateien bieten die beste Erfahrung für wiederkehrende Besucher, indem sie die Ressourcen cachen, aber der erste Besuch erfordert eine zusätzliche HTTP-Anfrage, die das Rendern der Seite blockiert.
  • Aktualisieren wir unsere Stile häufig? Alle gecachten .css-Dateien sind wertlos, wenn wir unsere Stile häufig aktualisieren und damit jeden Cache ungültig machen.
  • Wiederverwenden wir viele Stile und Komponenten? Atomares CSS glänzt, wenn wir viele CSS-Eigenschaften in unserer Codebasis wiederverwenden.

Die Beantwortung der oben genannten Fragen hilft uns zu entscheiden, nach welchen Funktionen wir bei der Auswahl einer CSS-in-JS-Lösung suchen müssen, und ermöglicht es uns, fundiertere Entscheidungen zu treffen.