UI-Frameworks sind Abstraktionen, die es uns erlauben, Oberflächen in Abhängigkeit von Daten zu beschreiben. Abstraktionen haben jedoch auch ihre Kosten. Langsame, unnötige Re-Renders in React oder unnötige Change-Detection-Zyklen in Angular zeigen, dass die Abstraktionen Lücken haben, die wir in der Entwicklung ausfüllen oder überspringen müssen.  

SolidJS verspricht anders zu sein. Das neue UI-Framework von Ryan Carniato (eBay) soll dabei helfen, hoch performante Oberflächen zu entwickeln, ohne den Komfort der anderen Frameworks zu verlieren. Wie das in einer Applikation aussieht und was das für Konsequenzen für den Entwicklungsprozess hat, zeigen wir in diesem Artikel. 

Noch ein Framework? 

Seit einigen Jahren schon sind wir müde. Müde, die jährlich neuen Sprachfeatures zu lernen, unsere Build-Chain an den nächsten großen Versionssprung von Webpack anzupassen oder beim nächsten Projekt schon wieder evaluieren zu müssen, welches Framework denn jetzt am besten geeignet sein könnte. Und jetzt soll noch ein weiteres Framework Platz in unserer Werkzeugkiste finden? Wieso sollten wir einer relativ unbekannten Bibliothek überhaupt Beachtung schenken? Um hier zu überzeugen, müssen wir uns zunächst den Status Quo anschauen. 

State-Management in UI-Frameworks 

Die größte Herausforderung in der UI-Entwicklung ist, volatile Daten so mit der Oberfläche zu verankern, dass wir in der Oberfläche niemals inkonsistente Zustände sehen. Sogar große Plattformen wie Facebook oder Twitter kämpfen mit diesem Problem, was wir merken können, wenn uns mal wieder eine rote Eins über Neuigkeiten informieren soll, die Liste der Benachrichtigungen aber leer ist. Frameworks gehen dieses Problem unterschiedlich an. Nehmen wir als Beispiel eine Anwendung mit dem folgenden Komponenten-Baum: 

Abbildung SolidJS: State-Management in UI-Frameworks

 

Auf der GitHubChecker-Komponente ist mit dem blauen Punkt ein Zustand markiert: Ein GitHub-Nutzername, der über die UserNameForm-Komponente geändert werden kann. Frameworks versprechen uns jetzt, dass wir diesen Zustand frei verändern können, während sich die Oberfläche „automagisch“ anpasst, um die aktuelle Version darzustellen.  

Nehmen wir zunächst Angulars standard Change-Detection-Strategie. Bei dieser sorgt jedes Event, welches von Angular bearbeitet werden soll, dafür, dass jede Oberflächenkomponente jeden dynamischen Ausdruck im Template erneut prüft, um festzustellen, ob es irgendwo Änderungen gab. Dadurch kann sehr einfach sichergestellt werden, dass wirklich jede Änderung von der Oberfläche abgebildet wird. Wirklich performant kann das aber nicht sein, vor allem, wenn wir große Applikationen mit vielen Komponenten umsetzen müssen.  

Bei Team-Blau sieht es etwas besser, aber auch nicht wirklich schön aus: React iteriert bei einer Zustandsänderung in einer Komponente über jede Kindkomponente, erzeugt eine neue Version des VDOM und vergleicht diese mit der vorherigen Version. Damit wird schon mal nicht die ganze Anwendung neu berechnet, wir müssen aber selbst darauf achten, Zustände nicht zu weit nach oben im Baum zu ziehen, vor allem wenn diese sich häufig ändern sollen. 

Beide Frameworks bringen die Möglichkeiten mit, in diesen Prozess einzugreifen, um unsere Apps performanter zu machen. Häufig ist das mit viel manueller Arbeit verbunden, und schöner wird unser Code dadurch auch nicht. 

Wie könnte eine andere Welt aussehen? Wenn wir uns ein perfektes UI-Framework backen würden, würden wir es vielleicht so zubereiten, dass wir zu jeder Zeit genau wissen, welche Zustände für welche Berechnungen und welche Oberflächen verwendet werden. Dann könnten wir bei einer Änderung eines Zustands immer direkt, ohne Umwege, wissen, welcher Code erneut ausgeführt werden muss. Und genau das macht SolidJS. 

Reaktive Grundbausteine 

Bevor wir mit der Beschreibung der Oberfläche anfangen, wollen wir uns zunächst die Definition der Zustände anschauen. Hier trifft Solid die Entscheidung, dass sich ändernde Daten explizit in Laufzeitcontainern definiert werden müssen. Anders als z.B. in Svelte prüft also kein Black-Box-Compiler, welche Abhängigkeiten es zwischen Ausdrücken gibt. 

Dafür benutzen wir in SolidJS die createSignal-Funktion. Das erinnert schon mal sehr an Reacts useState mit zwei entscheidenden Unterschieden: 

  1. In der name-Variable liegt nicht der aktuelle Wert des Zustands, sondern ein sogenannter „Accessor“. Das ist eine Funktion, die uns den aktuellen Wert zurückgibt. 
  2. createSignal ist in keiner Weise direkt mit Komponenten verknüpft. Wir können das Signal also auch direkt auf Modul-Ebene definieren und exportieren und haben so den Zustand global abgelegt, ohne eine weitere State-Management-Bibliothek wie Redux zu brauchen. 

Bisher können wir mit dem Signal noch nicht viel machen. Wir können Daten ablegen und auslesen. Eigentlich möchten wir aber auf Änderungen im Zustand reagieren können und genau das macht createEffect möglich: 

Mit createEffect können wir Reaktionen definieren, die immer ausgeführt werden sollen, wenn sich zugrundeliegende Daten ändern. Ähnelt mal wieder mal sehr Reacts useEffect, hat aber auch wieder zwei große Unterschiede: 

  1. Wir müssen nicht manuell Abhängigkeiten spezifizieren! Solid ist schlau genug, beim ersten Aufruf der Effekt-Funktion mitzuschreiben, welche Signals ausgelesen werden. Sobald ein Signal während eines Effekts ausgelesen wird, wird intern eine Abhängigkeit abgelegt. Solid weiß automatisch, dass bei jeder Änderung des count-Signals die Effekt-Funktion erneut aufgerufen werden muss. Ändert sich das Signal nicht, wird der Effekt nie wieder aufgerufen. 
  2. Auch hier sind wir wieder nicht an Komponenten gebunden! 

Zu guter Letzt wäre noch die Möglichkeit gut, abgeleitete Daten zu definieren. Und dafür ist createMemo zuständig: 

Auch hier ist die Ähnlichkeit mit React wieder zu sehen, wobei wir jedoch auch dieses mal keine Abhängigkeiten manuell definieren müssen: Solid merkt sich, welche Signale bei der Berechnung des Memos benötigt waren und weiß damit, dass calculateIsPrime nur dann aufgerufen werden muss, wenn sich das count-Signal ändert. Als Resultat erhalten wir einen neuen Accessor. In der Effekt-Funktion beziehen wir uns dann auf diesen isPrime-Accessor, sodass der Effekt immer ausgeführt wird, wenn sich der Wert von isPrime ändert. 

Mit diesen Grundbausteinen haben wir jetzt ein System, mit dem wir Daten definieren und Abhängigkeiten zwischen verschiedenen Datensätzen und Reaktionen auf Änderungen explizit abbilden können. Das müssen wir jetzt noch mit einer Template-Sprache verbinden, um ein fertiges Framework zu haben. 

Solid Komponenten: React ohne Overhead 

Bevor wir erklären, wie eine SolidJS-Komponente funktioniert, starten wir mit einer Quizfrage: Was sehen wir auf der Browser-Konsole, wenn wir die folgende Komponente im Browser darstellen? 

Wenn wir uns an unseren bisherigen React-Erfahrungen orientieren, würden wir erwarten, dass die Konsole zunächst 0 ausgibt, um dann im Sekundentakt bis 3 zu zählen. Doch das passiert nicht! Auf der Konsole erscheint nur die 0, während das h1-Element mit der 0 startet und dann im Sekundentakt bis 3 zählt. 

Wir denken nochmal ans vorherige Kapitel: Solid möchte, dass wir Code, der Abhängigkeiten auf Signale hat, explizit mit createEffect markieren! Unser erstes console.log ist aber nicht in einem Effekt, und reagiert damit nicht auf Änderungen. Anders formuliert: Der Funktionskörper einer Solid-Komponente wird nur einmal aufgerufen: Beim Setup der Komponente. Wenn wir jetzt nach unten auf den TSX-Teil gucken, sehen wir aber, dass Solid das Template dennoch schlank hält: Dynamische Ausdrücke im TSX werden vom Compiler automatisch in Effekte gewickelt. Um das Verhalten noch ein bisschen besser zu verstehen, schauen wir uns das folgende simplere Beispiel an: 

Der SolidJS-Compiler macht daraus das folgende JavaScript: 

Das sieht auf den ersten Blick vielleicht etwas kompliziert aus. Wenn wir hier Zeile für Zeile durchgehen, werden wir aber merken, wie simpel das Resultat eigentlich ist. 

Hier wird zunächst sämtlicher statischer HTML-Inhalt in ein natives HTMLTemplateElement umgewandelt. Dieser lässt sich mit der cloneNode-Funktion sehr performant im DOM einfügen und wir vermeiden so, dass statischer Inhalt mehrfach verarbeitet werden muss. 

Innerhalb der Komponente wird der TSX-Teil zunächst in eine Funktion verpackt, die direkt aufgerufen wird (IIFE genannt), damit unser geschriebener Code nicht mit den vom Compiler erzeugten Variablen verschmutzt wird. 

Als nächstes wird das HTML-Template geklont und in die Variable $el gelegt. Auch hier sehen wir wieder, dass die Button-Funktion selbst nur einmal pro Instanz aufgerufen werden kann, weil sonst mit jedem Aufruf ein neues HTML-Element erzeugt werden würde. Der ganze dynamische Teil der Komponente wird in den folgenden vier Zeilen aufgebaut: 

Zunächst wird der EventListener definiert. Das macht Solid über eine etwas vereinfachte Syntax. Die Bibliothek geht später Elemente durch und wandelt $$click in addEventListener- bzw. removeEventListener-Aufrufe um. 

An die insert-Funktion wird dann direkt das Signal übergeben. Innerhalb von insert wird nun anhand des Datentyps des Signals entschieden, wie das Element eingefügt werden soll. In unserem einfachen Fall passiert hier quasi nur createEffect(() => $el.textContent = count());. Also immer, wenn sich das count-Signal ändert, wird der Text unseres HTML-Elements ausgetauscht. 

In der dritten Zeile muss folgend noch innerhalb eines Effekts die ID des Elements aktualisiert werden und zuletzt wird das HTML-Element zurückgegeben. 

Zusammenfassend sehen wir also, dass Solid unsere Komponenten nur einmal pro Instanz aufruft und sämtliche Dynamik so isoliert, dass jede Änderung an Signals ohne Umwege direkt zu den Effekten geführt werden kann, die dann den DOM anpassen. 

Um diese Konzepte noch weiter zu verinnerlichen, empfehlen wir das offizielle, interaktive SolidJS-Tutorial auf der Webseite. 

Metriken und Benchmarks 

Das hört sich bis hierhin alles schon sehr gut an. Ohne weitere Zahlen können wir aber natürlich keine Technologieentscheidungen treffen. Daher nun ein paar Einblicke, die der Framework-Entwickler Ryan Carniato hier in mühsamer Arbeit zusammengetragen hat. Starten wir mit der (Brotli) komprimierten Dateigröße der Bibliotheken: 

Svelte  Solid  React 
1,85kb  3,86kb   

36,22kb 

 

 

Hier sehen wir schon, dass der Startpunkt der Bibliotheken schon stark unterschiedlich ist. Solid kommt zwar nicht ganz an den Ansatz von Svelte heran, ist aber dennoch in einer anderen Größenordnung als React. 

Neben der Bundlesize für die kleinstmögliche Anwendung müssen wir aber auch darauf achten, wie die Größe skaliert, wenn die Anwendung größer wird: 

Abbildung: SolidJS - Metriken und Benchmarks 

Metriken und Benchmarks

Das Diagramm zeigt uns die Gesamt-Bundlesize (Y-Achse) für eine Applikation, die X Komponenten enthält, die jeweils eine Todo-Applikation enthalten. Ganz links sehen wir wieder, dass Solid und Svelte mit einer gänzlich anderen Ausgangsbasis starten. Anhand der Steigung der Kurven erkennen wir das Skalierungspotential: Die Svelte-Kurve steigt mit zunehmender Anzahl an Komponenten relativ stark an, während Solid und React sehr flach verlaufen. Bei einer Applikation, die ca. 100kb groß ist, spielt es beispielsweise keine Rolle, ob wir Svelte oder React benutzen. Der mit dieser Grenze mögliche Funktionsumfang ist bei beiden Frameworks quasi gleich. Wir sehen also, dass vor allem bei sehr kleinen Applikationen die Wahl des Frameworks eine erhebliche Rolle spielen kann. So haben zum Beispiel 25 kleine Solid-Todo-Apps die gleiche Bundle-Size wie eine React-Todo-App. 

Werfen wir als nächstes noch einen Blick auf die Performance. Der JS-Framework-Benchmark vergleicht über 100 verschiedene Implementierungen verschiedener Aufgaben mit verschiedensten Frameworks und misst die Dauer von Interaktionen, die Zeit bis zur Interaktivität und den Speicherverbrauch. Schauen wir uns hier mal die Geschwindigkeit von Interaktionen an: 

Abbildung SolidJS: JS-Framework-Benchmark

JS-Framework-Benchmark

Wir sehen ganz links als Basiswert eine handoptimierte Implementierung in VanillaJS, also ohne Framework. Schneller als diese Baseline können wir also nicht werden. Dicht gefolgt werden die über 300 Zeilen JavaScript-Code jedoch von der nur 81 Zeilen langen, wesentlich lesbareren SolidJS-Alternative. Wir büßen also nur sieben Prozent Performance für eine erhebliche bessere Developer-Experience ein. 

In sämtlichen der vom JS-Framework-Benchmark abgedeckten Metriken ist SolidJS übrigens entweder das Beste der verbreiteten Frameworks (React, Angular, Vue, Svelte) oder quasi gleichauf mit einem Kontrahenten. Legen wir also großen Wert auf Performance und Bundle-Size, ist Solid definitiv keine falsche Wahl. 

Das perfekte Framework? 

Bisher sind wir nur auf die herausragenden Vorteile von SolidJS eingegangen. Natürlich muss man hier aber auch ehrlich sein und erwähnen, dass die Arbeit mit dem Framework nicht nur Vorteile hat.  

Der wohl größte Nachteil liegt wohl im Community-Support. Da SolidJS noch vergleichsweise jung ist (Veröffentlichung im April 2019), finden wir bei weitem nicht so viele Open-Source-Pakete wie für die bekannten Alternativen. So muss man in der Praxis häufig selbst Hand anlegen, wenn man zum Beispiel eine Font-Awesome-Integration oder eine Einbindung vom verbreiteten GraphQL-Client Apollo benötigt. Aufgrund der großen Ähnlichkeit zu React geht die Implementierung solcher Pakete zwar relativ schnell, ein npm install @apollo/react ist aber auf jeden Fall schneller.  

Desweiteren erfordert SolidJS ein gewisses Umdenken. Durch die starke Verbreitung von React sind wir in der Entwicklung einige Freiheiten gewohnt, auf die wir in Solid verzichten müssen, was gerade zu Beginn des Projektes zu Problemen führen kann. Der Verlauf der GitHub-Sterne des Projektes verrät jedoch, dass die Bibliothek immer bekannter wird, sodass dieser Schwachpunkt vielleicht bald nicht mehr so sehr ins Gewicht fallen muss. 

Wenn Sie das, was Sie bis hierhin gelesen haben, gereizt hat, empfehlen wir sehr, einfach mal mit dem Framework herumzuspielen. Am einfachsten geht das direkt im Browser mit dem Solid Playground. Wir wünschen an dieser Stelle viel Spaß mit dem neuen Werkzeug und genießen Sie die Befreiung vom Framework-Overhead! 

Letzte Artikel von Andreas Roth (Alle anzeigen)

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.

Die von Ihnen hier erhobenen Daten werden von der Host Europe GmbH zur Veröffentlichung Ihres Beitrags in diesem Blog verarbeitet. Weitere Informationen entnehmen Sie bitte folgendem Link: www.hosteurope.de/AGB/Datenschutzerklaerung/