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.
setCount(count + 1);
Nach dieser Zeile passiert im Browser erst einmal gar nichts. Kein Knoten wird ersetzt, kein Pixel neu gezeichnet. React ruft zunächst nur JavaScript-Funktionen auf – die Komponenten – und sammelt ein, was sie zurückgeben. Ob überhaupt ein DOM-Knoten angefasst werden muss, entscheidet sich erst danach, in einem eigenen Schritt. Dass eine Komponentenfunktion läuft und dass der Browser etwas zu tun bekommt, sind zwei verschiedene Vorgänge – an dieser Trennung lösen sich die meisten Missverständnisse über React-Performance auf.
Im Beitrag JSX genau: Die Regeln hinter dem Markup ging es darum, dass ein JSX-Element letztlich nur ein Objekt ist – eine Beschreibung dessen, was auf dem Bildschirm stehen soll. Hier geht der Weg weiter: Wie wird aus dieser Beschreibung ein echtes DOM, und was passiert damit, wenn sich der Zustand ändert? Rendern ist dabei eine Berechnung in JavaScript, das Ändern des DOM ein Schreibvorgang im Browser. React hält beides bewusst auseinander.
Zwischen einer Zustandsänderung und einer sichtbaren DOM-Änderung liegen mehrere klar getrennte Stufen.
- Auslösen · Zustand, Props oder Kontext machen Arbeit notwendig
- Rendern · React ruft Komponenten auf und erzeugt die nächste Elementstruktur
- Abgleichen · alte und neue Struktur werden anhand von Typ und Schlüssel verglichen
- Commit · notwendige Änderungen werden auf DOM und Effekte angewendet
Die Trennung erklärt, warum ein erneuter Komponentenaufruf nicht automatisch einen vollständigen DOM-Neuaufbau bedeutet.
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 root = createRoot(document.getElementById("root"));
root.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 Beitrag JSX genau 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, lässt sich ein Teilbaum von Hand memoisieren, etwa mit React.memo: React überspringt das erneute Rendern dann, solange sich dessen Props nicht geändert haben. Die Reihenfolge bleibt aber dieselbe: erst messen, dann prüfen, 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 – gibt es den Folgebeitrag Zustand erhalten und zurücksetzen.
Weiterführende Quellen
- Repository 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
Wie fandest du diesen Beitrag?
Kommentare