Startseite / Artikel / Wie der Browser zeichnet und wo React hineinpasst

Wie der Browser zeichnet und wo React hineinpasst

Erfahren Sie, wie der Critical Rendering Path, Reconciliation, Fiber und der Scheduler zusammenwirken, um React-Updates in Pixel auf dem Bildschirm umzuwandeln.

1798 Wörter

Zuerst: Vergessen Sie React – wie malt ein Browser eigentlich eine Seite?

Lassen Sie React für einen Moment beiseite. Selbst ein einfaches HTML-Dokument mit etwas CSS durchläuft eine feste Abfolge von Schritten, bevor ein einziger Pixel angezeigt wird. Jeder Browser folgt dieser Abfolge, unabhängig davon, mit welchen Werkzeugen die Seite erstellt wurde:

HTML → DOM tree
CSS  → CSSOM tree
DOM + CSSOM → Render Tree
Render Tree → Layout → Paint → Composite → Screen

Jede Phase erledigt eine bestimmte Aufgabe, und allein die Namen machen das nicht unbedingt klar:

  • DOM-Tree – der Browser analysiert Ihr HTML und wandelt es in einen Baum aus Knoten um. Dabei geht es ausschließlich um die Struktur: Welche Elemente befinden sich innerhalb welcher anderen.
  • CSSOM-Tree – derselbe Ansatz gilt auch für Ihre Styles. Jede von Ihnen erstellte CSS-Regel wird in einen Baum umgewandelt, den der Browser abrufen kann.
  • Render-Tree – der Browser kombiniert die beiden Bäume: Er durchläuft den DOM, fügt zu jedem Knoten die entsprechenden CSSOM-Regeln hinzu und überspringt alles, was tatsächlich nicht sichtbar sein wird (ein Element mit display: none existiert weiterhin im DOM, wird aber aus dem Render-Tree ausgeklammert).
  • Layout – das ist die arithmetische Phase. Unter Verwendung jedes Elements sowie seiner berechneten Styles bestimmt der Browser die genaue Position und Größe jedes Elements.
  • Paint – nun füllt der Browser die visuellen Elemente ein: Text, Hintergrundfarben, Ränder, Schatten usw.
  • Composite – wenn eine Seite mehrere Ebenen hat (was häufig aus Leistungsgründen der Fall ist), stapelt der Browser sie in der richtigen Reihenfolge, und das endgültige Ergebnis ist das, was tatsächlich auf Ihrem Bildschirm angezeigt wird.
  • Zusammen bilden diese Schritte das sogenannte Kritische Darstellungsverfahren, oder CRP.

    Diese Abfolge läuft unverändert ab, egal ob Sie React, Vue, reines jQuery oder überhaupt keine Bibliothek verwenden. Das Zeichnen der Pixel ist die Verantwortung des Browsers und nicht etwas, was ein UI-Framework übernimmt.

    Wo passt React also in dieses Ganze?

    Das ist der Teil, bei dem es einen Moment dauert, darauf zu klicken. React ersetzt das Kritische Darstellungsverfahren nicht – es arbeitet daran vorbei.

    Ohne React bedeutet das Aktualisieren der Benutzeroberfläche, dass man den entsprechenden DOM-Node manuell finden und selbst ändern muss:

    const counterEl = document.getElementById('counter');
    counterEl.textContent = newCount;
    

    Das ist bei einem einzelnen Zähler noch handhabbar. Stellen Sie sich aber ein Dashboard vor, bei dem vierzig separate Werte unabhängig voneinander geändert werden können – dann müsste man jeden einzelnen Wert manuell verfolgen und aktualisieren. Genau dieses Problem zu lösen, ist der Grund für das Bestehen von React.

    Mit React sieht derselbe Update stattdessen so aus:

    function Counter({ count }) {
      return <p>{count}</p>;
    }
    

    Man beschreibt, wie die Benutzeroberfläche anhand der aktuellen Daten aussehen sollte, und berührt den DOM niemals direkt. Daher muss etwas anderes diese Arbeit übernehmen. Das ist die eigentliche Aufgabe von React, und sie findet bereits vor dem Beginn des kritischen Renderingspfades des Browsers statt:

    State changes → React figures out what changed → applies a small patch to the real DOM
                                                                  ↓
                              browser does its normal thing: Layout → Paint → Composite → Screen
    

    Der gesamte Wertvorschlag von React besteht darin, diesen ersten Schritt – herauszufinden, was sich geändert hat – so schnell und präzise wie möglich durchzuführen, damit der Browser nur den Layout- und Paint-Vorgang für den kleinen Teil der Seite wiederholen muss, der dies tatsächlich benötigt, anstatt alles erneut verarbeiten zu müssen.

    Imperativ gegenüber deklarativ: der Denkwechsel

    Dieser Kontrast erklärt, warum React so konzipiert wurde.

    Imperativer Code beschreibt jeden einzelnen Schritt ausführlich:

    list.innerHTML = '';
    for (const item of items) {
      const li = document.createElement('li');
      li.textContent = item;
      list.appendChild(li);
    }
    

    Es liegt an Ihnen zu entscheiden: Löschen Sie das, erstellen Sie dieses Element und fügen Sie es hier hinzu.

    Deklarativer Code beschreibt hingegen das gewünschte Ergebnis:

    <ul>
      {items.map(item => <li key={item}>{item}</li>)}
    </ul>
    

    Anstatt dem Browser Anweisungen zu geben, „ein li zu erstellen und es einzufügen“, besagen Sie stattdessen: „Gegeben dieses Array, so sollte die resultierende Benutzeroberfläche aussehen.“ Etwas anderes muss diese Beschreibung in konkrete DOM-Operationen umwandeln – und zu verstehen, was das ist, kommt als Nächstes.

    Was „Reconciliation“ wirklich bedeutet

    Der Ansatz klingt komplizierter, als er tatsächlich ist, sobald man ihn direkt betrachtet.

    React speichert ein mentales Abbild Ihrer Benutzeroberfläche, das als Virtual DOM bezeichnet wird. Immer wenn sich etwas ändert, erstellt React eine neue Version dieses Baums und vergleicht sie mit der vorherigen, um herauszufinden, was sich geändert hat. Dieser Vergleichsschritt wird als Reconciliation bezeichnet.

    Der Überprüfung aller möglichen Unterschiede zwischen zwei Bäumen wäre rechenintensiv, daher nutzt React einen Shortcut mit zwei Regeln, die in der Praxis für eine schnelle Ausführung sorgen:

    1. Der Elementtyp hat sich geändert (zum Beispiel ein `

    ` in ``) — React schaut nicht einmal auf die Kinderelemente; es verwirft den alten Knoten vollständig und erstellt einen neuen.

    1. Der Elementtyp bleibt derselbe (`

    werden zu

    `) — React behält den vorhandenen echten DOM-Knoten bei und repariert nur die abweichenden Teile.

    Eine Falle, die fast jeden betrifft, haben Listen. Standardmäßig vergleicht React die Listeneinträge anhand ihrer Indizes – Eintrag 0 mit Eintrag 0, Eintrag 1 mit Eintrag 1 und so weiter. Wenn Sie einen neuen Eintrag am Anfang einer nicht mit einem Schlüssel versehenen Liste einfügen, geht React davon aus, dass sich auch alle darunterliegenden Einträge geändert haben.

    Deshalb sollten Sie den Listeneinträgen immer einen stabilen key zuweisen:

    {items.map(item => <li key={item.id}>{item.name}</li>)}
    

    Sobald die Einträge einen Schlüssel haben, kann React erkennen, „dass sich dieser bestimmte Eintrag nur in der Position geändert hat“, anstatt anzunehmen, dass die gesamte Liste neu erstellt wurde.

    Nach all diesen Vergleichen erhält React schließlich eine kurze, gezielte Reihe von Anweisungen – wie zum Beispiel „diesen Textknoten aktualisieren“ oder „einen Knoten hier einfügen“ – und das sind die Operationen, die tatsächlich auf dem echten DOM angewendet werden.

    Ist das nicht dasselbe, was Fiber tut?

    Es ist eine berechtigte Frage, über die man nachdenken sollte.

    Die Versöhnung ist die zugrundeliegende Idee. Fiber ist lediglich das Werkzeug, das sie umsetzt.

    Vor React 16 wurde dieses Werkzeug Stack Reconciler genannt. Es durchsuchte den gesamten Baum rekursiv und synchron, was bedeutet, dass es einmal im Gange war, bis zum Ende durchlaufen musste, bevor es stoppen konnte. Bei großen Updates konnte dies den Hauptthread so lange blockieren, dass die Anwendung träge wirkte – Frames fielen aus und das Tippen schien unresponsiv zu sein.

    Fiber, das in React 16 eingeführt wurde, ersetzte dieses Verarbeitungssystem. Das Konzept der Abstimmung änderte sich nicht, doch nun wird die Arbeit in kleine Abschnitte aufgeteilt, die pausiert, verworfen oder später wieder aufgenommen werden können. Wenn etwas Dringenderes auftaucht – wie zum Beispiel das Eintippen des Benutzers – kann React die Arbeit mit geringerer Priorität unterbrechen, sich um den dringenden Update kümmern und anschließend dort weitermachen, wo es aufgehört hatte.

    Daher ist es nicht korrekt zu behaupten, Fiber habe die Abstimmung ersetzt. Genauer ist es, zu sagen, dass der ältere Motor, der die Abstimmung durchführte, durch einen leistungsfähigeren ersetzt wurde.

    Was macht also der Scheduler?

    Fiber macht es möglich, Arbeit zu pausieren und wieder aufzunehmen, doch etwas anderes muss entscheiden wann gepausiert werden soll und welche Aufgabe Vorrang hat. Das ist die Aufgabe des Schedulers.

    Zu seinen Verantwortlichkeiten gehören:

    • Ranking-Updates nach Dringlichkeit – beispielsweise wird eine Eingabe über die Tastatur als dringend betrachtet, während ein Refresh einer entfernten Hintergrundliste es nicht ist.
    • Ausfüllen der Lücken zwischen den Browser-Frames, um schrittweise an weniger wichtigen Aufgaben voranzukommen, und Zurückweichen, bevor der nächste Frame gerendert werden muss.
    • Einschalten von Funktionen von React 18 wie startTransition, wobei das Markieren eines Updates als nicht dringend dem Scheduler effektiv mitteilt, dass er diese Aufgabe nach unten auf der Prioritätenliste verschieben darf.

    Ein einfaches mentales Modell verbindet diese drei Konzepte miteinander:

    Reconciliation  → the algorithm (what changed?)
    Fiber           → the engine that makes that algorithm interruptible
    Scheduler       → the traffic controller deciding when to pause/resume Fiber's work
    

    Render-Phase versus Commit-Phase

    Es gibt noch eine weitere Unterscheidung, die verstanden werden sollte: Fiber teilt seine Arbeit in zwei Phasen auf, die völlig unterschiedliche Regeln folgen.

    Die Render-Phase ist der Zeitpunkt, an dem die eigentliche Differenzierung stattfindet. React ruft Ihre Komponentenfunktionen auf, stellt den neuen Baum zusammen und vergleicht ihn mit dem vorherigen. Dabei wird die eigentliche Seite noch nicht berührt, weshalb diese Phase sicher pausiert, übersprungen oder neu gestartet werden kann.

    In der Commit-Phase schreibt React schließlich in den echten DOM ein und wendet die berechnete Änderung an. Diese Phase kann nicht unterbrochen werden – sie läuft von Anfang bis Ende in einem einzigen, ununterbrochenen Durchlauf ab, da eine teilweise angewendete UI-Änderung die Seite in einem fehlerhaften visuellen Zustand zurücklassen würde. Unmittelbar nach der Aktualisierung des DOMs, aber bevor der Browser den Bildschirm ausgibt, wird useLayoutEffect synchron ausgeführt. Im Gegensatz dazu wird useEffect etwas später ausgeführt, nachdem der Browser bereits mit dem Ausgeben fertig ist.

    Brauchen Sie React überhaupt?

    Ehrlich gesagt nicht immer. Viele Produktionswebsites laufen ausschließlich mit HTML, CSS und reinem JavaScript und funktionieren einwandfrei.

    React rechtfertigt seinen Overhead erst, wenn die Anforderungen komplexer werden:

    • Die manuelle Verwaltung von DOM-Updates ist bei kleinen Projekten noch handhabbar, wird aber unübersichtlich, wenn man mit Dutzenden von miteinander verbundenen UI-Elementen umgehen muss.
    • Ein großer Teil der tatsächlichen UI-Fehler entsteht dadurch, dass Zustand und angezeigte Benutzeroberfläche aus dem Gleichgewicht geraten. Reacts Ansatz – die Benutzeroberfläche als Funktion des Zustands zu betrachten und das Framework den Vergleich zu übernehmen – beseitigt durch seine Konzeption einen großen Teil dieses Risikos.
    • Die Möglichkeit, wiederverwendbare Komponenten zu erstellen, unterstützt durch ein Ökosystem aus Routing-Tools, Entwicklertools und gemeinsamen Konventionen, wird wertvoll, sobald mehr als ein Entwickler an derselben Codebasis arbeitet.

    Für eine einfache Landing-Page oder eine größtenteils statische Website ist jedoch reiner JavaScript die bessere Wahl. Die Einführung von Fiber, dem Scheduler sowie des vollständigen Reconciliation-Pipelines würde bedeuten, unnötige Kosten für ein Problem zu tragen, das man eigentlich nie hatte.

    React ist nicht per se besser als JavaScript. Es handelt sich um ein Werkzeugset, das dazu entwickelt wurde, ein spezifisches Problem zu lösen: die Synchronisierung der Benutzeroberfläche mit ständig sich ändernden Zuständen in großem Maßstab und innerhalb eines Teams. Unter dieser Skala erledigen reines HTML, CSS und JS die Aufgabe hervorragend.

    Verwandte Artikel

  • Vermeiden von stillen Zustandsfehlern durch Veränderung von JavaScript-Referenzen — Erfahren Sie, warum die Veränderung von Objekten und Arrays über Referenzen die erneute Darstellung in React stört, warum das Spread-Operator nur eine oberflächliche Kopie erstellt und wie man Zustände sicher tief klonen kann.
  • Eine praktische Vergleichsanalyse von React-Ordnungsstrukturmustern — Erklärt projektbezogene, schichtbasierte sowie domänenbasierte Ordnungsstrukturen in React und gibt Richtlinien zur Auswahl der richtigen Struktur im Laufe des Wachstums Ihrer Anwendung.