cs.Tutorial();

Svelteder leichtgewichtige Compiler für performante Webanwendungen

  • Svelte
  • JavaScript

Svelte ist gar nicht mal so neu, wie es vielleicht der aktuelle Hype um das Framework vermitteln mag. Das Komponenten-Framework wurde in seiner ersten Version bereits im Jahr 2016 veröffentlicht, gewann jedoch deutlich an Popularität mit der dritten Version in 2019. Dieser Beliebtheit trug vor allem der komplett neue Ansatz bei, wie Webanwendungen gebaut und ausgeliefert werden können. Laut Rich Harris, dem Erfinder und Entwickler von Svelte, ist Svelte, nämlich kein Framework im herkömmlichen Sinne, sondern ein Compiler. Der deklarative Anwendungscode mit Framework-spezifischen Direktiven und Anweisungen wird nicht, wie in anderen Frameworks, erst ausgeliefert und anschließend im Browser interpretiert, sondern in einem Build-Prozess in performantes Vanilla JavaScript, CSS und HTML übersetzt. Die resultierende Bundle-Größe kann somit sehr klein gehalten werden, wodurch sich wesentlich bessere Lade- und Verarbeitungszeiten im Browser erzielen lassen.

Grundlagen

Die einfache Syntax einer in Svelte geschriebenen Komponente ähnelt auf den ersten Blick einer gewöhnlichen HTML-Datei. Sie hat die Dateierweiterung .svelte und besteht im Wesentlichen aus drei Teilen: dem <script>-Tag, in dem die JavaScript-Logik enthalten ist, dem <style>-Tag für das Komponenten-spezifische CSS und dem Template der Komponente, in dem das Markup über geschweifte Klammer-Ausdrücke mit dynamischen Inhalten aus dem <script>-Tag angereichert werden kann. Der Aufbau einer einfachen Komponente für die Abbildung eines Todo-Listeneintrags kann Listing 1 entnommen werden.

<script>
let title = 'Svelte lernen';
</script>
<style>
.icon {
margin-right: 0.5rem;
}
</style>
<li><span class="icon"></span>{title}</li>
Listing 1: TaskList.svelte – der Aufbau einer Svelte-Komponente

Eine andere Svelte-Komponente kann im <script>-Bereich über die import-Anweisung eingebunden und im Template als Tag verwendet werden. Hierbei bedient sich das Framework der Upper-Camel-Case Namenskonvention, um Eigene von den Standard-HTML-Tags zu unterscheiden. Listing 2 zeigt den Import der <Task>-Komponente und ihre Verwendung im Template der Komponente <TaskList>.

<script>
import Task from './Task.svelte';
</script>
<ul>
<Task/>
</ul>
Listing 2: TaskList.svelte – Import der Task-Komponente

Um Properties zu definieren und sie von außen zugängig zu machen, nutzt Svelte die export-Anweisung aus dem ES6-Standard für ES-Module. Damit die Aufgabenbeschreibung zur Anzeige an die Komponente <Task> weitergereicht werden kann, muss die Variable title exportiert werden (Listing 3).

<script>
export let title;
</script>
...
Listing 3: Task.svelte – Variable title als Property definieren

Eine Property wird beim Verwenden der entsprechenden Komponente wie ein übliches Tag-Attribut angegeben (Listing 4). Wird sie nicht mit einem Default-Wert bei der Deklaration belegt, wie im oberen Beispiel, so gilt sie als obligatorisch – der Verwender muss zwingend einen Wert angeben, andernfalls erscheint eine Warnmeldung in der Konsolenausgabe des Browsers.

<script>
import Task from './Task.svelte';
</script>
<ul>
<Task title="Svelte lernen"/>
</ul>
Listing 4: TaskList.svelte – Property title setzen

Mit speziellen #if- und :else-Blöcken lassen sich im Template Inhalte abhängig von einer Bedingung ein- und ausblenden. Auf diese Weise kann, wie im Listing 5 dargestellt, neben der Aufgabenbeschreibung entweder ein Kreis oder ein Häkchen-Emoji eingeblendet werden.

<script>
export let title;
export let done = false;
</script>
<style> ... </style>
<li>
<span class="icon">
{#if done}
✔️
{:else}
{/if}
</span>
{title}
</li>
Listing 5: Task.svelte – Das Häkchen- oder Kreis-Emoji anzeigen

Über Datenlisten zu iterieren und sie in geeigneter Form darzustellen, gehört zu den Standardaufgaben heutiger Frameworks. Svelte stellt hierfür die Block-Anweisung #each zur Verfügung. Im Listing 6 wird über das Array tasks iteriert und pro Eintrag eine Task-Komponente mit entsprechenden Angaben initialisiert.

<script>
import Task from "./Task.svelte";
let tasks = [
{title: 'Svelte lernen', done: true},
{title: 'App mit Svelte entwickeln', done: false}
];
</script>
<style> ... </style>
<ul>
{#each tasks as task}
<Task title={task.title} done={task.done}/>
{/each}
</ul>
Listing 6: TaskList.svelte – alle Aufgaben aus dem tasks-Array darstellen

Eine Aufgabe auf einer To-do-Liste muss auch irgendwann abgehakt werden. Für eine Anwendung bedeutet dies, dass es möglich sein muss, auf Benutzerinteraktionen zu reagieren und diese entsprechend zu verarbeiten. In Svelte können mit der Direktive on: können Events abgefangen und an korrespondierende Event-Handler weitergeleitet werden. Eine mögliche Verarbeitung von click-Events auf einem Listeneintrag kann z.B. wie im Listing 7 dargestellt umgesetzt werden.

<script>
export let title;
export let done = false;
function toggleDone() {
done = !done;
}
</script>
<li on:click={toggleDone}> ... </li>
Listing 7: Task.svelte – auf click-Events reagieren

Die Zustandsänderung innerhalb der <Task>-Komponente bewirkt zwar, dass das Häkchen-Emoji wie erwartet erscheint, die übergeordnete <TaskList>-Komponente würde jedoch von dieser Anpassung nichts mitbekommen – die Daten im task-Array blieben somit auf dem alten Stand. Um dies zu beheben, könnte man entweder die Eltern-Komponente das click-Event selbst verarbeiten lassen oder ein benutzerdefiniertes Event werfen und sie somit über die Zustandsänderung benachrichtigen. Die erste Variante ist einfach umzusetzen. Um ein Event an die Eltern-Komponente weiterzuleiten, muss nur die Direktive on: und das weiterzuleitende Event (ohne Event-Handler) angegeben werden (Listing 8).

...
<li on:click > ... </li>
...
Listing 8: Task.svelte – click-Event an die übergeordnete Komponente weiterleiten

In der Eltern-Komponente kann so das click-Event abgefangen und verarbeitet werden (Listing 9).

...
<ul>
{#each tasks as task}
<Task title={task.title}
done={task.done}
on:click={() => toggleDone(task)} />
{/each}
</ul>
Listing 9: TaskList.svelte – das weitergeleitete click-Event abfangen

Die zweite Möglichkeit dies zu tun, wäre das Auslösen eines benutzerdefinierten Events in der Komponente <Task>. Dafür muss zunächst die Factory-Methode createEventDispatcher aus dem svelte-Paket importiert und über deren Aufruf eine neue dispatch-Methode erzeugt werden. Anschließend kann diese überall in der Komponente verwendet werden, um benutzerdefinierte Events auszulösen. Im Listing 10 wird im click-Handler der Komponente zunächst die Property done negiert und anschließend ein neues Event mit dem Bezeichner toggleDone und den aktualisierten Daten der Aufgabe als Payload ausgelöst.

import { createEventDispatcher } from 'svelte';
...
let dispatch = createEventDispatcher();
function toggleDone() {
done = !done;
dispatch("toggleDone", { title, done });
}
...
Listing 10: Task.svelte – benutzerdefiniertes Event auslösen

Die Deklaration des entsprechenden Event-Listeners in der Eltern-Komponente <TaskList> ist die Gleiche, wie bei einem click-Event. Sie setzt sich aus der Direktive on: und dem definierten Bezeichner des Events toggleDone zusammen. Das an die dispatch-Methode übergebene Objekt kann im Event-Handler über die Event-Property detail wieder abgerufen werden (Listing 11).

...
<ul>
{#each tasks as task}
<Task title={task.title}
done={task.done}
on:toggleDone={(e) => toggleDone(e.detail)} />
{/each}
</ul>
Listing 11: TaskList.svelte – benutzerdefiniertes Event verarbeiten

Reaktivität

Auch wenn in Svelte zusätzliche Features, wie z.B. Routing integriert werden können, beschränkt sich das Framework im Wesentlichen auf die Komponentenentwicklung. Vor diesem Hintergrund lässt es sich am besten mit React vergleichen, weil dieses ebenfalls als reines Komponenten-Framework betrachtet werden kann, in dem zusätzliche Features nach Bedarf hinzugefügt werden können. Beide Frameworks schreiben sich Reaktivität ganz groß auf die Fahne, letzteres trägt es sogar im Namen.

Im Gegensatz zu React setzt Svelte kein virtuelles DOM ein, um Elemente in der baumartigen Struktur des echten DOM zu aktualisieren. Stattdessen fährt das Leichtgewicht, wie im Folgenden beleuchtet, eine deutlich simplere und zugleich leistungsfähigere Strategie. Doch warum das Rad neu erfinden und nicht das Altbewährte einsetzen? Ist virtuelles DOM etwa nicht schnell genug?

React und der Overhead des virtuellen DOM

Das Verfahren bei dem der vorhergehende mit dem aktualisierten Stand des virtuellen DOM abgeglichen und im Falle einer Abweichung, die Änderung in das echte DOM übertragen wird, ist allgemein unter dem Begriff Reconciliation (Ausgleich, Abstimmung) bekannt. Dieser Vorgang wird dann in Gang gesetzt, sobald in einer Komponente die Methode setState() aufgerufen wurde. Der Aufruf stößt eine Verarbeitungskette an, die sich im Wesentlichen wie folgt beschreiben lässt.

Zunächst wird die betroffene Komponente mit einem dirty-Flag gekennzeichnet. Auf diese Weise landet sie in einer Stapelverarbeitung zu aktualisierender Komponenten. Nach einigen Auswertungen, ob und wie die Komponente aktualisiert werden soll, wird jedes einzelne in der Komponente enthaltene Eltern- und dessen Kindelemente betrachtet, und deren Stand vor und nach der Aktualisierung des Zustands verglichen. Nur dann, wenn sich die Stände unterscheiden, findet eine Aktualisierung des Elements im DOM statt.

Dieser Vorgang hat sich bis heute deshalb bewährt, weil er ein präzises Eingreifen in das DOM ermöglicht, anstatt ganze Zweige zu ersetzen oder zu aktualisieren, was in Bezug auf Performance wesentlich teurer ist. Dennoch bedeutet es jedes Mal, dass überflüssige Arbeit verrichtet werden muss, um eine Aktualisierung vorzunehmen. Besteht beispielsweise die zu aktualisierende Komponente insgesamt aus zehn Elementen und es hat sich nur der Text einer Schaltfläche geändert, so würde die Reconciliation dazu führen, dass neun von zehn Elementen – ihre Attribute und Inhalt – unnötig betrachtet wurden. Wesentlich ineffizienter erscheint jedoch die Tatsache, dass die render()-Methode bei jeder State-Änderung zur Ausführung kommt und somit jedes Mal aufs Neue alle dort enthaltenen Funktionen und Variablen neu definiert werden. Frameworks, die auf dieser Technologie aufsetzen, haben unweigerlich mit einem Speicher- und Performance-Overhead zu kämpfen, was sich nicht zuletzt über entsprechende API-Methoden, wie shouldUpdateComponent, React.PureComponent, useMemo und useCallback kenntlich macht.

Reaktivität in Svelte

Svelte verwendet keinen Intermediär, um das DOM auf dem aktuellen Stand zu halten. Der Compiler weiß bereits zur Build-Zeit, wie sich die Dinge in der Anwendung ändern können und muss es nicht erst zur Laufzeit umständlich ermitteln. Im Build-Prozess wird der Anwendungscode um ein paar Anweisungen ergänzt, die dafür sorgen, dass nur jene Elemente im DOM aktualisiert werden, die von der Zustandsänderung auch betroffen sind. Es setzt keine Hook-Methoden ein, wie setState, um eine Aktualisierung anzustoßen. Stattdessen bleibt das Framework seinem Namen treu und benutzt einfach das Gleichheitszeichen – den Zuweisungsoperator. Sobald sich der Wert einer Variable durch eine neue Zuweisung ändert, werden automatisch alle Template-Elemente im DOM neu gezeichnet, die diese Variable verwenden.

Um an dieser Stelle noch einen draufzusetzen, hat das Team hinter Svelte zusätzlich ein Feature eingeführt, mit dem ganze Anweisungen reaktiv deklariert werden können. Das leicht verstaubte Label-Statement wurde in Svelte neu entdeckt und vielleicht auch ein wenig zweckentfremdet. Zumindest wurde es bisher meistens im Kontext von Schleifen in Verbindung mit break und continue eingesetzt. In Svelte hingegen wird ein mit $: gelabeltes Statement als reaktiv interpretiert und immer dann ausgeführt, wenn irgendeine Variable in diesem Statement geändert wurde. Auf diese Weise lassen sich dynamische Zuweisungen, wie im Listing 12 abgebildet, leicht umsetzen.

let a = 10;
$: b = a + 1;
a = 100;
console.log(b); //=> 101
Listing 12: Reaktive Anweisungen in Svelte

Die Anweisung auf der rechten Seite der Zuweisung a + 1 wird automatisch ausgeführt und das Ergebnis in die Variable b geschrieben, sobald der Variable a ein neuer Wert zugewiesen wurde. Dies hat wiederum zur Folge, dass alle Elemente im Template, welche die Variable b referenzieren, augenblicklich aktualisiert werden – was das Label-Statement zu einem mächtigen Werkzeug macht, um reaktive Benutzeroberflächen zu implementieren.

Objekte und Arrays

Da jede Aktualisierung der Benutzeroberfläche mit einer Zuweisung an die jeweilige Variable eingeleitet werden muss, stellt sich unweigerlich die Frage, wie Modifikationen in verschachtelten Strukturen wie Objekten und Arrays erfolgen müssen, um eine Aktualisierung des DOM anzustoßen. Auch hier muss zwingend eine Zuweisung an die im Template referenzierte Variable erfolgen, um das Neuzeichnen entsprechender UI-Elemente in Gang zu setzen. Ein Array oder ein Objekt zu manipulieren und es dann der entsprechenden Variable erneut zuweisen, würde zwar in Svelte ebenfalls zum Ziel führen, allerdings würde sich jede statische Code-Analyse über die redundante Zuweisung beschweren. Erfreulicherweise sind auf dem Array.prototype viele Methoden definiert, die ein Array unterschiedlich manipulieren und am Ende eine neue Instanz zurückgeben können.

Am Beispiel der Aufgabenliste anknüpfend, kann eine mögliche Manipulation eines Task-Objekts im tasks-Array z.B. unter Verwendung der map-Methode, wie im Listing 13 dargestellt, erfolgen.

...
function update(changedTask) {
tasks = tasks.map(task => {
if (task.title === changedTask.title ) {
task.done = changedTask.done;
}
return task;
});
}
...
Listing 13: TaskList.svelte – Objekte im Array richtig aktualisieren

Das richtige Maß an Unterstützung

Barrierefreiheit

Als jemand, der täglich Code schreiben und lesen muss, wird man aller Wahrscheinlichkeit nach nur selten motorisch eingeschränkt oder eine starke Sehbeeinträchtigung haben, sodass man auf einen Screenreader oder ähnliche Hilfsmittel angewiesen wäre. Deshalb ist es für viele Entwickler schwierig, sich vorzustellen oder zumindest jedesmal daran zu denken, wie jemand mit solchen Einschränkungen, die Software, die sie schreiben, barrierefrei bedienen kann. In Svelte ist Accessibility ein fester Bestandteil des Workflows. Mit Warnungen im Build-Prozess sowie während der Entwicklung mit Hinweisen in der IDE (vorausgesetzt passende Erweiterungen sind installiert) weist das Framework auf nachzubessernde Stellen im Code hin und trägt damit nicht unwesentlich zur Verbesserung der Barrierefreiheit der eigenen Anwendung bei.

Würde man z.B. einen <img>-Tag definieren, ohne das Attribut alt anzugeben, so würde sowohl beim Bauen der Applikation als auch in der IDE eine entsprechende Warnung erscheinen:

A11y: <img> element should have an alt attributes

Styling

Keine Webanwendung kommt heutzutage ohne Styling aus. Längst gehört CSS zu den Standardtechnologien, die ein Entwickler von Webanwendungen beherrschen sollte. Doch nicht alle Frameworks, die sich dem Thema Webentwicklung gewidmet haben, haben out-of-the-box auch eine geeignete Antwort auf Probleme, die mit CSS in komplexen Applikationen einhergehen. Der übermächtige globale Geltungsbereich, das ewige Tauziehen verschiedener Selektoren und die Beseitigung des nicht verwendeten Codes sind längst nicht alle Herausforderungen, mit denen man zu kämpfen hat. Frameworks, die hierfür keine Lösung haben, sind nicht komplett. Svelte hingegen bringt CSS-Kapselung auf Komponentenebene out-of-the-box und unterstützt den Entwicklungsprozess mit entsprechenden Hinweisen zu obsoleten CSS-Definitionen.

In Svelte ist es nicht erforderlich mit Präfixen, wie es z.B. in CSS-Systemen, BEM oder SMACSS üblich ist, zu arbeiten. Die CSS-Kapselung findet stattdessen völlig automatisch, unter der Haube statt. Für jede Komponente wird ein Hashwert berechnet und als CSS-Klasse an jedes formatierte Element im Template angehangen. Die im <style>-Tag definierten Selektoren werden mit der berechneten CSS-Klasse erweitert und werden somit eindeutig.

<style>
li {
color: red;
}
</style>
<li>{task}</li>
Styling-Definition in einer Svelte-Komponente
li.svelte-19xqvng {
color: red;
}
Ausschnitt aus der kompilierten CSS-Datei
<li class="svelte-19xqvng">Svelte lernen</li>
Ergebnis im DOM

Animationen und Übergänge

Zu einer guten Usability und User Experience gehören nicht nur eine sinnvolle Wahl und Anordnung von UI-Elementen, ebenso ist es von Bedeutung zu erkennen, wenn in der Maske neue Elemente erscheinen, verschwinden oder von A nach B bewegt werden. Das menschliche Auge nimmt solche Bewegungen am besten wahr, wenn sie nicht ruckartig sondern flüssig ablaufen. Um dieser Tatsache gerecht zu werden, ist es also besonders wichtig, Animationen und Übergänge im richtigen Maße und an geeigneten Stellen in der Benutzeroberfläche einzusetzen. Auch hier unterstützt das Framework den Entwicklungsprozess, in dem es einfache Animationen und Übergänge bereitstellt, die beim Kompilieren in CSS-Transitions und -Animations übersetzt werden. Außerdem besteht die Möglichkeit auf eine einfache Art und Weise eigene Animationen zu implementieren. Das folgende Beispiel zeigt den Import und die Anwendung einer fade-Transition, die automatisch dann zur Ausführung kommt, wenn eine neue Aufgabe in das Array tasks hinzugefügt oder aus ihm entfernt wird (Listing 15).

<script>
import { fade } from "svelte/transition";
...
</script>
<style> ... </style>
<li on:click={toggleDone} transition:fade>
...
</li>
Listing 15: fade-Transition einsetzen

Weiterführende Projekte

Neben dem Hauptprojekt Svelte existieren noch einige weitere Projekte, die auf dem Framework aufbauend die Entwicklung mit Web-Technologien ein gutes Stück nach vorne treiben.

Sapper

Das Sapper-Projekt ist ein Pendant zu Next.js. Es ist ein Framework für SSR (Server-Side-Rendering) Web-Applikationen mit einem Code-Splitting-Ansatz, Dateisystem ähnlichen URLs und einem besonderen Augenmerk auf Suchmaschinenoptimierung (SEO). Da Sapper auf Svelte aufbaut und somit alle Vorteile des Compilers ausnutzen kann, muss im Gegensatz zu Next.js nicht der komplette Framework-Code in einem der initialen Code-Split-Chunks geladen werden. Die deutlich kleineren Bundles, die höhere Performance und Speicher-Effizienz, machen Sapper zu einem mächtigen Allrounder für die Entwicklung von SSR-Web-Applikationen.

Svelte Native

Mit Svelte Native können mobile Apps unter Verwendung des NativeScripts implementiert werden. Dem grundlegenden Ansatz von Svelte folgend wird auch hier die wesentliche Arbeit in einem Kompilierungs-Schritt des Build-Prozesses verrichtet, anstatt sie auf mobile Geräte zu verlagern.

Svelte GL

Mit High-Level-Bibliotheken wie Three.js und regl lassen sich inzwischen animierte 3D-Grafiken erzeugen und im Webbrowser anzeigen. Doch diese auf WebGL aufbauenden Bibliotheken gehen oft mit einem Overhead an zu implementierendem JavaScript einher, dessen schlechte Lesbarkeit und Wartbarkeit nur selten für nebenläufige Grafiken gerechtfertigt werden kann. Svelte GL hingegen verfolgt einen Ansatz, bei dem solche Grafiken auf eine deklarative Art und Weise – unter Verwendung entsprechender Tags – in Webseiten und Anwendungen leicht eingebunden werden können. Das Projekt befindet sich aktuell noch im experimentellen Stadium und ist eher eine Spielwiese, die dazu dienen soll die richtigen APIs zu bestimmen und das Zusammenspiel mit dem Svelte Compiler auszutesten.

Fazit

Vermutlich würde man heute noch keine große Enterprise-Anwendung mit Svelte umsetzen wollen. Dafür existieren aktuell noch keine ausgereiften, auf Svelte basierenden UI-Kits, was bei der Wahl eines Front-End-Frameworks in sehr vielen Projekten eine ausschlaggebende Rolle spielen würde. Man hat einfach selten das Budget und die notwendige Zeit, um Standard-Komponenten wie Buttons, Modale, Slider, Trees und Navbars selbst implementieren und testen zu können.

Trotzdem ist der neue Compiler-Ansatz von Svelte ein bemerkenswerter Fortschritt in die richtige Richtung. Den Anwendungs-Code zur Build-Zeit zu übersetzen und somit deutlich kleinere und schnellere Bundles ausliefern zu können, kann nicht nur auf leistungsschwächeren Geräten wie Smart-TVs und Handhelds zu einer besseren Performance und User-Experience beitragen.

Zur Blog-Post Übersicht