Wie React den Baum rendert und aktualisiert
Vom JSX-Element zum sichtbaren Pixel – was zwischen einer Zustandsänderung und dem neu gezeichneten Bildschirm genau passiert. Render und Commit als getrennte Phasen, der Abgleich zweier Bäume und warum React fast immer nur einen winzigen Teil des DOM anfasst.
Wie React den Baum rendert und aktualisiert
Im JSX-Beitrag habe ich gezeigt, dass ein JSX-Element am Ende nur ein Objekt ist – eine Beschreibung dessen, was auf dem Bildschirm stehen soll. Was aber passiert danach? Wie wird aus dieser Beschreibung ein echtes DOM, und vor allem: Was macht React, wenn sich der Zustand ändert und die Oberfläche neu aussehen soll? Genau diese Mechanik ist der Grund, warum React-Anwendungen schnell bleiben, obwohl bei jeder Änderung scheinbar alles neu berechnet wird. Dieser Beitrag geht den Weg vom Element zum Pixel Schritt für Schritt durch.
Der wichtigste Gedanke vorweg, weil er alles andere ordnet: Rendern und das Ändern des DOM sind bei React zwei verschiedene Dinge. Das eine ist eine Berechnung in JavaScript, das andere ein Schreibvorgang im Browser. React trennt beide bewusst, und wer diese Trennung versteht, versteht die halbe Performance-Geschichte.
Drei Schritte: Auslösen, Rendern, Anwenden
React zeigt eine Oberfläche in drei Schritten an. Man kann sie sich wie eine Bestellung in einer Küche vorstellen: Jemand gibt die Bestellung auf, die Küche kocht, der Kellner bringt das Gericht an den Tisch.
flowchart LR Trigger["1. Auslösen<br/>initial oder setState"] --> Render["2. Rendern<br/>Komponenten aufrufen"] Render --> Commit["3. Anwenden<br/>minimale DOM-Änderung"] Commit --> Paint["Browser zeichnet neu"]
Der erste Schritt ist das Auslösen. Es gibt genau zwei Anlässe. Beim Start der Anwendung passiert es einmalig, wenn man die Wurzel erzeugt und die oberste Komponente übergibt. Danach wird ein erneutes Rendern nur noch durch eine Zustandsänderung ausgelöst – immer über die Setter-Funktion aus useState oder einen entsprechenden Mechanismus. Ohne Zustandsänderung rendert React nichts neu. Das ist wichtig: Ein Klick allein bewirkt nichts; erst wenn der Klick den Zustand ändert, kommt die Maschine in Gang.
import { createRoot } from "react-dom/client";
const wurzel = createRoot(document.getElementById("root"));
wurzel.render(<App />);
Rendern heißt: React ruft deine Funktionen auf
Der zweite Schritt, das Rendern, ist der, der am meisten missverstanden wird. Rendern bedeutet bei React nicht, etwas auf den Bildschirm zu malen. Rendern bedeutet: React ruft deine Komponentenfunktionen auf, um herauszufinden, was angezeigt werden soll. Beim ersten Mal ruft es die Wurzelkomponente auf. Bei einem erneuten Rendern ruft es die Komponente auf, deren Zustand sich geändert hat.
Und dieser Aufruf ist rekursiv. Gibt eine Komponente andere Komponenten zurück, ruft React auch die auf, und deren Kinder ebenfalls, bis es keine verschachtelten Komponenten mehr gibt. Am Ende dieses Durchlaufs steht kein DOM, sondern ein neuer Baum aus React-Elementen – denselben Beschreibungsobjekten aus dem JSX-Beitrag, nur eben der ganze Baum davon.
flowchart TB App["App()"] --> Liste["Rechnungsliste()"] App --> Kopf["Kopfzeile()"] Liste --> Z1["Zeile() #1"] Liste --> Z2["Zeile() #2"] Z1 --> Betrag1["Betrag()"] Z2 --> Betrag2["Betrag()"]
Damit dieser Durchlauf verlässlich ist, verlangt React eine Eigenschaft, die ich in den früheren Beiträgen schon betont habe: Das Rendern muss eine reine Berechnung sein. Für dieselben Props und denselben Zustand muss dieselbe Beschreibung herauskommen, und beim Rendern darf nichts verändert werden, was vorher schon existierte. Nur unter dieser Bedingung kann React das Rendern gefahrlos wiederholen, aufschieben oder mit einem späteren Ergebnis verwerfen – alles Dinge, die die moderne, nebenläufige Variante von React tatsächlich tut.
Der Abgleich: zwei Bäume vergleichen
Jetzt kommt der Kern, der erklärt, warum das Ganze schnell ist. Nach einem erneuten Rendern hat React zwei Bäume: den neuen, gerade berechneten, und den vorherigen aus dem letzten Durchlauf. Bevor irgendetwas ins DOM geschrieben wird, vergleicht React die beiden. Diesen Abgleich nennt man Reconciliation, und er folgt ein paar einfachen Regeln.
An derselben Stelle im Baum entscheidet der Typ des Elements. Ist der Typ gleich – vorher ein <div>, jetzt wieder ein <div>, oder vorher wie jetzt dieselbe Komponente –, dann behält React den bestehenden Knoten und aktualisiert nur die Eigenschaften, die sich geändert haben. Ist der Typ verschieden – vorher ein <p>, jetzt ein <div> –, dann gibt es keinen Abgleich im Detail: React reißt den alten Teilbaum ab, wirft dessen Zustand weg und baut den neuen von Grund auf.
flowchart LR
subgraph Vorher
A1["div"] --> A2["span: 'Hallo'"]
end
subgraph Nachher
B1["div"] --> B2["span: 'Welt'"]
end
A1 -. "gleicher Typ:<br/>Knoten bleibt" .-> B1
A2 -. "nur Textinhalt<br/>ändert sich" .-> B2
Diese Regel hat eine praktische Folge, über die Anfänger oft stolpern. Weil ein Typwechsel den ganzen Teilbaum verwirft, verliert man dabei auch dessen Zustand: Ein Eingabefeld, das vorher in einem <div> steckte und jetzt in einem <section>, wird als neues Element behandelt und ist wieder leer. Wer will, dass Zustand erhalten bleibt, muss dafür sorgen, dass an derselben Stelle derselbe Typ steht.
Listen sind der Sonderfall – und hier kommen die Keys
Bei Kindern in einer Liste reicht die Position nicht aus, denn Listen ändern ihre Reihenfolge. Genau dafür gibt es den Key, über den ich im JSX-Beitrag schon gesprochen habe – und hier zeigt sich, warum er so wichtig ist. Beim Abgleich einer Liste vergleicht React nicht Position mit Position, sondern Key mit Key. Ein Element mit demselben Key gilt als dasselbe Element wie vorher, egal an welcher Stelle es jetzt steht. React verschiebt dann den bestehenden DOM-Knoten, statt ihn wegzuwerfen und neu zu bauen.
Deshalb ist der Array-Index als Key so tückisch. Fügt man vorne ein Element ein, verschiebt sich jeder Index um eins. Für React sieht es dann so aus, als hätte sich der Inhalt jeder Zeile geändert – und der Zustand, der eigentlich zur dritten Rechnung gehörte, klebt plötzlich an der vierten. Mit einer stabilen, fachlichen ID als Key erkennt React die Identität korrekt: Das eingefügte Element ist neu, alle anderen sind dieselben wie vorher und wandern einfach eine Position weiter.
Anwenden: React fasst nur das an, was sich geändert hat
Erst nach dem Abgleich kommt der dritte Schritt, der Commit. Jetzt schreibt React ins echte DOM – aber nur das, was der Abgleich als Unterschied ergeben hat. Beim allerersten Rendern muss alles neu erzeugt werden, dafür hängt React die erzeugten Knoten mit den üblichen DOM-Aufrufen in die Seite. Bei jedem weiteren Mal gilt der entscheidende Satz aus der Dokumentation: React verändert einen DOM-Knoten nur, wenn es zwischen zwei Renderdurchläufen einen Unterschied gibt.
Das klingt selbstverständlich, hat aber sichtbare Konsequenzen. Wenn eine Elternkomponente neu rendert, ein Eingabefeld tief darin aber unverändert bleibt, dann fasst React diesen Knoten nicht an – und der Text, den man gerade getippt hat, bleibt stehen, obwohl der ganze Ast neu gerechnet wurde. Genau das ist der Beweis, dass Rendern und DOM-Änderung getrennt sind: Die Komponente wurde neu gerendert (die Funktion lief), aber das DOM wurde nicht angefasst (es gab keinen Unterschied).
sequenceDiagram participant N as Nutzer participant R as React participant D as DOM N->>R: setState (Zustand ändert sich) R->>R: Komponenten erneut aufrufen (rendern) R->>R: neuen Baum gegen alten abgleichen R->>D: nur die Unterschiede schreiben D->>N: Browser zeichnet das Ergebnis
Nach dem Commit ist React fertig. Was danach passiert, gehört dem Browser: Er zeichnet den geänderten Teil der Seite neu. Dieses Neuzeichnen ist ein eigener, von React unabhängiger Schritt – ein weiterer Grund, warum die minimale DOM-Änderung so viel wert ist, denn je weniger sich im DOM ändert, desto weniger muss der Browser neu malen.
Warum ein erneutes Rendern nicht teuer sein muss
Mit diesem Bild löst sich ein verbreitetes Missverständnis auf. Viele glauben, ein erneutes Rendern sei automatisch langsam, und optimieren wild dagegen an. Aber Rendern heißt zunächst nur, dass eine JavaScript-Funktion noch einmal läuft und ein paar Objekte erzeugt. Das ist selten das Problem. Teuer wird erst das Schreiben ins DOM und das anschließende Neuzeichnen – und genau das hält React durch den Abgleich klein.
Das heißt nicht, dass Rendern gratis ist. Rendert eine hoch angesiedelte Komponente neu, ruft React den ganzen Teilbaum darunter erneut auf, auch wenn am Ende kaum DOM-Änderungen herauskommen. Für die allermeisten Anwendungen ist das kein Thema. Wo es doch messbar wird, kann React ein erneutes Rendern eines Teilbaums überspringen, wenn sich dessen Props nicht geändert haben – früher von Hand mit einer Memoisierung, heute zunehmend automatisch durch den React Compiler, der diese Fälle beim Bauen erkennt. Die Reihenfolge der Optimierung ist aber wichtig: erst messen, dann überlegen, ob wirklich das Rendern das Problem ist, und meistens ist es das nicht.
Was man daraus mitnimmt
Die ganze Mechanik lässt sich auf einen Satz eindampfen: React rechnet bei jeder Zustandsänderung eine neue Beschreibung der Oberfläche aus, vergleicht sie mit der alten und schreibt nur die Unterschiede ins DOM. Die Beschreibung ist billig, der Vergleich ist clever, und die teure DOM-Arbeit bleibt minimal. Der Key ist dabei kein Detail, sondern die Information, mit der der Vergleich Identität von Position unterscheidet – und der Grund, warum eine gute Wahl des Keys über korrekt erhaltenen Zustand entscheidet.
Wer das im Kopf hat, trifft im Alltag bessere Entscheidungen: Man erschrickt nicht mehr vor einem erneuten Rendern, man weiß, warum ein Eingabefeld seinen Text behält oder verliert, und man versteht, wann ein Typwechsel oder ein wechselnder Key einen ganzen Teilbaum wegwirft. Zum tieferen Verhalten von Zustand über Rendergrenzen hinweg – warum Zustand an eine Position im Baum gebunden ist und wann er zurückgesetzt wird – schreibe ich einen eigenen Folgebeitrag.
Weiterführende Quellen
- Übungsrepository introduction-react (Beispiele zum Nachvollziehen): https://github.com/mikebild/introduction-react
- React, Render und Commit: https://react.dev/learn/render-and-commit
- React, Zustand als Momentaufnahme: https://react.dev/learn/state-as-a-snapshot
- React, Zustand erhalten und zurücksetzen: https://react.dev/learn/preserving-and-resetting-state
- React, Listen rendern (Keys): https://react.dev/learn/rendering-lists
Kommentare
Kommentar schreiben