Über unsMediaKontaktImpressum
Manuel Ernst 08. April 2025

Svelte – Das reaktive Frontend-Framework und seine Entwicklung

Dies ist der erste Teil einer Serie zum Thema Svelte. Wir werden uns zuerst Grundsätzliches zum Thema Svelte, insbesondere im Bezug auf das Rendermodell, ansehen und im zweiten Teil explizit darauf eingehen, wie in Svelte 5 bestimmte Schwächen des Reaktivitäts-Modells angegangen wurden und welche weiteren Verbesserungen in Bezug auf Features und Performance-Verbesserungen in Svelte 5 eingeflossen sind.

Überblick Web-Frameworks

Schaut man sich die Geschichte der Web-Frontend-Frameworks der letzten 15 Jahre an, so ist offensichtlich, dass sich das Framework React innerhalb weniger Jahren zum De-facto-Standard im Bereich Frontend-Entwicklung etabliert hat: Der jährlichen Erhebung "State-of-JS" ist zu entnehmen, dass seit 2016 durchgehend mindestens 99 Prozent der Befragten bekannt sind mit dem Framework [1]. Diese Vorreiterrolle musste React auch bis heute weder in Bezug auf Bekanntheit noch auf den tatsächlichen Einsatz abtreten.

Schaut man sich jedoch an, wie sich das Interesse der Entwicklergemeinde an React verändert hat, ergibt sich ein etwas anderes Bild: Im letzten Jahr (2024) war das Interesse auf ein Rekordtief von nur 34 Prozent gefallen.

Zu diesem Verlauf haben zahlreiche neue Frameworks entscheidend beigetragen, allen voran das Framework Svelte. Es liegt zwar in der Verwendung noch lange nicht an erster Stelle, doch bei der Mehrheit der befragten Entwickler belegt es seit mehreren Jahren kontinuierlich den Spitzenplatz in Sachen Beliebtheit. Grund dafür ist unter anderem der radikal abweichende Ansatz in Bezug auf die Herangehensweise, wenn es um das Rendern von Oberflächen im Browser geht.

Rendering: Imperativ vs. Deklarativ

Wenn eine Webanwendung Informationen im Browser darstellen möchte, muss sie sich des DOMs (= Document Object Models) bedienen. Das DOM ist eine Schnittstelle, über die die Kommunikation mit dem Browser abgewickelt wird und damit Veränderungen am vorliegenden Dokument möglich sind. Dabei kann es um das Hinzufügen, das Verändern oder Entfernen von Dokument-Knoten gehen. Im folgenden Beispiel wird mithilfe der DOM-Schnittstellen ein Button erzeugt und im vorliegenden Dokument eingefügt.

const root = document.getElementById('root')
let clickCount = 0
const button = document.createElement('button');
const t1 = document.createTextNode('clickCount: ')
const t2 = document.createTextNode(clickCount)
button.appendChild(t1)
button.appendChild(t2)
button.onclick = () => {
  clickCount++;
  t2.textContent = clickCount
};
root.appendChild(button)

Das DOM an sich ist imperativ angelegt, das bedeutet, dass explizite Anweisungen formuliert werden müssen, um Intentionen wie z. B. "füge einen Button hinzu” zu kommunizieren. Bei wenig komplexen Anwendungsfällen wie dem Button aus dem obigen Beispiel ist der Ansatz der imperativen Programmierung einfach und effizient, wird aber sehr aufwändig, wenn viele Stellen von einer möglichen Zustandsänderung betroffen sind und der Entwickler im Kopf behalten muss, welche DOM-Knoten erstellt, gelöscht oder verändert werden müssen. Allein im obigen Beispiel gibt es bereits zwei Zeitpunkte, zu denen der Text des Buttons gesetzt werden muss! Aus diesem Grund wählt man einen deklarativen Ansatz, um die Darstellung einer Anwendung zu beschreiben. Umschreiben lässt sich der imperative gegenüber dem deklarativen Ansatz mit einem Beispiel aus der "echten" Welt: "Eine Wand soll weiß gestrichen werden."

Die imperative Vorgehensweise ist wie folgt:

  • Gehe in den Baumarkt und kaufe weiße Farbe und einen Pinsel!
  • Öffne den Farbeimer!
  • Nimm den Pinsel!
  • Tauche ihn in die Farbe!
  • Bewege den Pinsel über die Wand!
  • Wiederhole die beiden vorherigen Schritte, bis die Wand vollständig weiß ist!

Diese Anweisungen für das Streichen einer Wand sind vergleichbar mit den DOM-Anweisungen aus dem obigen Beispiel: Jedes Detail muss berücksichtigt und explizit ausgeführt werden. Geht man die Aufgabe jedoch deklarativ an, führt der Weg über den Malermeister Siegfried Velte. An ihn stellen wir die Anforderung: "Ich möchte, dass diese Wand weiß ist." Welche Schritte im Detail erfolgen müssen, um zum Ziel einer weißen Wand zu gelangen, ist dabei aber irrelevant. Der Malermeister kennt sich bestens mit seinem Werkzeug und den Hilfsmitteln aus und weiß genau, welche Arbeitsschritte erforderlich sind.

Übertragen auf das Rendern von Webanwendungen im Browser ist der Malermeister ein Frontend Framework wie Svelte, die deklarative Anweisung an das Framework geschieht über eine Metasprache. In Svelte würde man das Button-Beispiel so ausdrücken [2]:

<script>
  let clickCount = 0
</script>

<button onclick={() => clickCount++}>
  clickCount: {clickCount}
</button>

Svelte übernimmt das Erzeugen des Button-Knotens genauso wie das Binden des Eventhandlers und die Aktualisierung der Beschriftung, wenn immer clickCount aktualisiert wird. Dabei muss der Entwickler KEINE konkreten Anweisungen formulieren, die an den DOM ergehen sollen und muss sich auch nicht im Detail um jede Stelle kümmern, die von einer Veränderung betroffen sein könnte.

Runtime vs. Buildtime

Kommen wir nun zu dem Ansatz, der Svelte von etablierten Frameworks wie zum Beispiel React unterscheidet: Wir hatten uns im vorherigen Kapitel angesehen, warum der deklarative Ansatz zur Definition von UIs dem imperativen Ansatz überlegen ist. Nachdem für die deklarative Definition eine Abstraktion notwendig ist (in React kommt zum Beispiel JSX zum Einsatz, während in Svelte eine Template-Syntax verwendet wird), ist ein weiterer Transformationsschritt notwendig, um aus dieser Abstraktion Anweisungen an das DOM abzuleiten. Nach wie vor ist ja die DOM-Schnittstelle der einzige Weg, um das Dokument zu beeinflussen, entsprechend muss vom Framework ermittelt werden, welche dieser DOM-Operationen auszuführen sind. Diese Ermittlung kann entweder zur Laufzeit der Anwendung passieren oder bereits zum Build-Zeitpunkt.

Runtime

Der Ansatz, der zum Beispiel von React verwendet wird, ist ein Runtime-Ansatz, auch DOM Diffing genannt. Das Framework geht so vor, dass bei jeder Änderung am Dokument der aktuelle Stand des DOMs verglichen wird mit einer virtuellen Repräsentation des zu erzeugenden Standes, auch virtuelles DOM genannt. Wenn diese beiden Stände abweichen, wird ermittelt, welche Veränderungen am DOM vorgenommen werden müssen, um den Wunschzustand zu erreichen.

Im folgenden Beispiel vergleicht React den Button aus dem obigen Beispiel mit dem aktuellen Stand: Im DOM existiert bisher kein Button, entsprechend spiegelt der Unterschied zwischen DOM und Virtual DOM wider, dass der Button bisher nicht existiert. Daraus ergeben sich eine Reihe von DOM-Anweisungen, wie zum Beispiel die Anlage des Knoten, das Setzen des Textes usw. 

Noch klarer wird das Konzept des DOM Diffings im zweiten Beispiel: Nach einem Button-Click wurde der Clickcounter erhöht. Der Algorithmus stellt fest, dass bereits ein Button existiert, jedoch die Beschriftung des Buttons abweicht. Daraus abgeleitet wird also nur die Anweisung, den Text des Buttons zu aktualisieren. Der Button, der schon existiert, wird vom Framework also direkt adressiert und nur bezüglich seiner Beschriftung aktualisiert.

Diese Ermittlung des Unterschiedes geschieht hier im Fall von React zur Laufzeit der Anwendung. Dieser Vergleich wird also bei jeder Zustandsaktualisierung der Anwendung eingestellt werden. Zwei Nachteile bringt dieser Ansatz mit sich: Zum einen gehen diese Diff-Operationen zu Lasten der Performance. Insbesondere wenn mal eine größere Liste von Entitäten angezeigt werden muss, impliziert das eine linear ansteigende Anzahl von zusätzlichen Vergleichen, die sich ultimativ beim Benutzer bemerkbar machen. Zum Zweiten ist es notwendig, den Framework-Code, der diese Laufzeit-Operationen durchführt, an den User auszuliefern, was sich in gestiegenen Ladezeiten niederschlägt.

Buildtime

Der Svelte-Ansatz verfolgt dagegen einen Buildtime-Ansatz: Ein Compiler inspiziert die Template-Syntax einer Komponente und ermittelt zum einen das zu erzeugende Markup und zum anderen den Zustand der Komponente, durch den Veränderungen am Markup getrieben werden. Im Beispiel des Buttons von oben wäre das die Variable clickCount. Es gilt sowohl bei der Anlage der Komponente, den Button mit dem Clickcount zu versehen als auch die Beschriftung bei jeder Veränderung des Clickcounts zu aktualisieren.

Weil durch die Template-Syntax von Svelte eindeutig identifizierbar ist, welche DOM-Knoten von welchen Zustandsänderungen betroffen sind, lässt sich dann ganz einfach im Voraus schon definieren, welche DOM-Operationen auszuführen sind.

Beim ersten Anzeigen einer Komponente wird erstmal grundsätzlich der Button erzeugt, mit Inhalt versehen und in das Dokument eingehängt. Es ist ja bekannt, dass noch nichts existiert, also ist auch kein Vergleich mit dem Ist-Zustand notwendig. Wenn clickCount verändert wird, ist ebenfalls bekannt, was zu verändern ist, auch hier ist kein Vergleich mit dem Ist-Zustand notwendig. Als Resultat erhält man eine hoch performante Anweisung, die nur genau die Logik ausführt, die notwendig ist, um die Anwendung anzuzeigen und gleichzeitig eine sehr schlanke Anwendung, weil in dem generierten Bundle, das an den User geschickt wird, nur ein sehr kompakter Runtime Layer enthalten ist. 

Reactivity

Ein Herzstück von Svelte ist das Konzept der Reaktivität. Hinter dem Begriff verbirgt sich die Art und Weise, wie Svelte in der Lage ist, auf Veränderungen in der Anwendung zu reagieren und diese wiederum in der Oberfläche widerzuspiegeln. Im Folgenden werden wir nur sehr kurz ein paar Beispiele aus Svelte 4 betrachten, um an das vorherige Kapitel anzuknüpfen, um dann tiefer auf die Neuigkeiten einzugehen, die Svelte 5 mitgebracht hat.

State Updates

Im vorherigen Kapitel haben wir uns das Button-Beispiel angesehen: Bei jedem Klick wird eine Variable erhöht, die dann wiederum in der Beschriftung des Buttons widergespiegelt wird. Bei React wird dem Framework explizit mitgeteilt, dass eine Änderung am Zustand passieren soll:

function CountButton() {
  const [clickCount, setClickCount] = useState(0)

  return (
    <button onClick={() => setClickCount(clickCount => clickCount + 1)}>
      clickCount: {clickCount}
    </button>
  )
}

Der Aufruf der Methode setClickCount bewirkt, dass React zum einen den Zustand aktualisiert und zum anderen, dass ein Rerender der Komponente veranlasst wird. Dies muss explizit passieren. Im Svelte-Beispiel jedoch ist dies nicht der Fall, die einzige Logik, die tatsächlich implementiert werden muss, ist die Veränderung der Statusvariablen:

<button onclick={() => clickCount++}>...</button>

Wir erinnern uns, dass jeglicher Svelte-Code erst einmal durch einen Compiler laufen muss, der aus dem Template Markup dann DOM-Anweisungen ableitet. Zusätzlich schaut sich der Compiler an, welche Zustandsvariablen in der Komponente existieren, wo sie im Markup verwendet werden und wo sie verändert werden. Nachdem der Compiler in der Lage ist, beliebigen Code zu generieren, fügt der Compiler in der Clickhandler-Methode eine kleine Code-Passage ein, die wiederum die Operation(en) veranlasst, die oben in der Tabelle zusammengefasst wurde(n). Bei jeder Veränderung von clickCount wird also die ermittelte Anweisung (das Aktualisieren der Buttonbeschriftung) veranlasst.

Abgeleiteter Zustand / Derived State

Oft ist es notwendig, aus Zustandsvariablen andere Zustände abzuleiten. Wir möchten unser Button-Beispiel so erweitern, dass zusätzlich zu der Anzahl der Klicks auch das Doppelte der Klickzahl angezeigt wird. Folglich muss diese Zahl bei jedem Klick, also bei jeder Änderung der Klickzahl, neu berechnet werden. Das Vorgehen bei React ist simpel: Weil eine Komponente bei jeder Zustandsänderung neu gerendert wird, kann man diesen abgeleiteten Zustand einfach bei jedem Renderdurchgang neu berechnen lassen:

const [clickCount, setClickCount] = setState(0)
const doubledClickCount = clickCount * 2

Auch wenn dieser Ansatz geradlinig und einfach nachvollziehbar ist, impliziert er doch einige Nachteile: Zum Beispiel muss die Neuberechnung jedes Mal angestellt werden, wenn die Komponente neu gerendert wird. Auch wenn sich die Variable clickCount gar nicht verändert hat. Und wenn die Berechnung aufwändiger ist als nur eine Multiplikation mit zwei, kann dies erhebliche Implikationen bei der Render-Performance nach sich ziehen.

In Svelte wird daher so vorgegangen, dass bestimmte Ausdrücke als reaktives Statement markiert werden. Der Svelte Compiler erkennt diesen Marker, identifiziert alle State-Variablen, von denen der Ausdruck abhängig ist, und generiert wiederum Code, der eine Neuberechnung des abgeleiteten Status veranlasst [3].

<script>
  let clickCount = 0
  $: doubledClickCount = clickCount * 2
</script>

<button onclick={() => clickCount++}>
  clickCount: {clickCount} / doubledClickCount: {doubledClickCount}
</button>

Durch den $:-Ausdruck wird die Verdopplungs-Berechnung als reaktiv markiert. Somit wird die Neuberechnung des Wertes immer dann ausgelöst, wenn sich clickCount verändert, während die Neuberechnung von doubledClickCount wiederum eine DOM-Aktualisierung auslöst. Sehr gut lässt sich diese Verkettung von Aktualisierungen und Abhängigkeiten mit dem Tabellenkalkulationsprogramm Excel vergleichen: Der Inhalt von Zelle A wird durch eine Formel bestimmt, die auf dem Inhalt von Zelle B basiert. Ändert sich Zelle B, bewirkt dies automatisch eine Aktualisierung des Inhalts von Zelle A.

Tangente: $:

Auch wenn es nicht so aussieht, der Ausdruck, der durch $: eingeleitet wird, ist valides, reguläres Javascript. Es handelt sich hierbei um ein sogenanntes Label, mit dem bestimmte Punkte im Code markiert werden können. Labels sind ein Javascript-Feature, das nur extrem selten, in bestimmten Nischenfällen, zum Einsatz kommt. Ein Beispiel wären zwei geschachtelte for-Schleifen:

loop1:
for (i = 0; i < 3; i++) {
  loop2:
  for (j = 0; j < 3; j++) {
    if (xyzCondition) {
      continue loop1;
    }
    ...
  }
}

In dem Anwendungsfall soll die äußere Schleife fortgesetzt werden, wenn xyzCondition wahr ist. Würde man das continue-Schlüsselwort ohne das Label benutzen, würde man sich auf die innere Schleife beziehen und dann die innere Schleife verlassen, anstatt die äußere. Insgesamt sind Labels ein so selten genutztes Mittel, dass man sich das Label $ als Marker ausgesucht hat, um reaktive Ausdrücke zu markieren. Dieser Marker ist grundsätzlich reserviert und kann nirgendwo anders im Svelte-Kontext verwendet werden.

Reactivity Wrap-Up

Der Svelte Compiler nutzt verschiedene generische Javascript-Konstrukte, um Intentionen und Verhalten einer Komponente abzuleiten. Diese JavaScript-Sprachmittel werden nur explizit im Svelte-Kontext, also wenn der Code vom Svelte Compiler konsumiert worden ist, mit dieser Bedeutung überladen. Ein Label beispielsweise zieht überhaupt keine Änderung am Verhalten des Codes nach sich, sondern bewirkt nur in einem ganz konkreten Fall, dass sich der Code anders verhält.

Mit der Einführung von Svelte 5 hat man sich von diesem Ansatz verabschiedet und sich für eine Methode entschieden, bei der die Intentionen des Entwicklers expliziter ausgedrückt werden. Mit diesen Details und weiteren umfassenden Verbesserungen in Bezug auf Entwicklungs-Ergonomie und Performance werden wir uns im zweiten Teil der Svelte-Serie beschäftigen.

DevOps auf den diesjährigen IT-Tagen

Spannende Vorträge und Workshops zum Thema DevOps erwarten Euch auch auf den IT-Tagen, der Jahreskonferenz von Informatik Aktuell. Die IT-Konferenz findet jedes Jahr im Dezember in Frankfurt statt – dieses Jahr vom 08.-11.12.

Autor
Das könnte Sie auch interessieren
Kommentare (0)

Neuen Kommentar schreiben