Startseite / Artikel

Denken im Kontext der Ungültigkeitsprüfung: Ein praktisches Modell dafür, wie Browser Seiten darstellen

Lernen Sie, über die Darstellung im Browser nachzudenken, indem Sie herausfinden, welche Änderungen was ungültig machen – von Layout-Problemen und Zeichnungskosten bis hin zu Kompositing, Hydratierung und Streaming.

Verlag: REACTAPP.TOP11 Min. Lesezeit2533 Wörter

Quellen: The Web Page Rendering Process In Browsers: A Deep Dive

Fast jeder Frontend-Entwickler kann den Renderingschritt für Schritt aufzählen: HTML wird zum DOM, CSS wird zum CSSOM, danach folgen Layout, Paint und Composite. Diese Abfolge ist eine gute Einführung, doch sie ist fast nutzlos, wenn eine echte Seite Probleme macht – schließlich beschreibt sie nur einen einzigen Durchlauf, während Browser ständig mehrfache, teilweise wiederholte Durchläufe durchführen. Dieser Leitfaden ersetzt die auswendig gelernte Liste durch ein funktionierendes Modell, das auf einer einzigen Frage basiert, sodass Sie Probleme wie Layout-Probleme, langsame Renderungen, schwache Animationen sowie Verzögerungen bei der Aktualisierung der Seite mit derselben Logik erklären und Ihre Vermutungen in DevTools überprüfen können.

Das Rendering ist kontinuierlich, kein einmaliger Vorgang

Eine Seite wird niemals einmal gerendert und danach unverändert gelassen. Skripte verändern den DOM, Klassenerweiterungen ändern die Styles, Bilder und Web-Schriften kommen verspätet an, Benutzer bewegen den Mauszeiger und scrollen, Animationen laufen in jedem Frame ab, und Frameworks rendern Komponenten nach Zustandsänderungen erneut. Jedes dieser Ereignisse zwingt den Rendering-Engine, dieselbe Frage zu beantworten:

Was ist nicht mehr gültig, und wie viel Arbeit muss erneut verrichtet werden?

Diese Frage im Hinterkopf zu behalten, ist wertvoller als die Kenntnis der internen Bezeichnungen. Deshalb können zwei identisch aussehende Anwendungen auf demselben Telefon völlig unterschiedlich performen: Die eine führt ständig teure Neuberechnungen durch, die andere nutzt größtenteils Arbeit, die der Browser wiederverwenden kann.

Schritt eins: Der Browser erstellt interne Modelle

Wenn HTML ankommt, wandelt der Parser es in das DOM um. Das DOM ist kein Bild der Seite – es handelt sich dabei um einen Baum aus Elementen, Attributen und Textknoten sowie um die Beziehungen zwischen Eltern- und Geschwisterelementen. Schauen wir uns ein kleines Fragment der Markup-Sprache an:

<main>
  <h1>Browser Rendering</h1>
  <p>Understanding what happens after HTML arrives.</p>
</main>

Der Parser wandelt es in einen Baum um, den der Engine abfragen und aktualisieren kann:

Document
└── main
    ├── h1
    │   └── "Browser Rendering"
    └── p
        └── "Understanding what happens after HTML arrives."

Stylesheets durchlaufen ihre eigene Verarbeitung. Der Browser analysiert sie, wendet Kaskadierung und Vererbung an, bestimmt, welche Regeln auf welche Elemente zutreffen, und berechnet den endgültigen Wert jeder Eigenschaft für jedes Element. Zu diesem Zeitpunkt wurde noch nichts dargestellt – die Engine sammelt lediglich Zustände und benötigt sowohl die Struktur als auch die berechneten Styles, bevor geometrische Eigenschaften bestimmt werden können. Ein abhängigkeitsorientierter Überblick sieht so aus:

HTML ──→ DOM ───────────┐
                        ├──→ Style + Layout ──→ Paint ──→ Composite
CSS  ──→ Styles ────────┘

Betrachten Sie dieses Bild eher als Karte der Abhängigkeiten denn als Beschreibung der Implementierung. Reale Engines enthalten viele mehr interne Schritte, Caches, Strukturen zur Verfolgung von Ungültigkeiten, mehrere Threads sowie plattformspezifische Abkürzungen. Was das Diagramm richtig darstellt, ist, welche Ausgaben von welchen Eingaben abhängen. Wenn Sie genauer erfahren möchten, wie Stilwerte vor dem Layout berechnet werden, geht unser Artikel zu dem Platzbedarf von CSS im Browser-Pipeline-Verfahren tiefer darauf ein.

Layout ist eine Abhängigkeitsberechnung

Sobald DOM und berechnete Styles verfügbar sind, berechnet der Browser die Geometrie. Beim Layout müssen folgende Fragen geklärt werden:

  • Welche Breite und Höhe hat jede Box?
  • Wo wird jede Box platziert?
  • Wo bricht der Text innerhalb eines Containers ab?
  • Wo enden die Kinder eines Elements?
  • Wie beeinflusst die Größe eines Elements seine Nachbarn – durch Druck oder Zug?
  • Die letzte Frage erklärt, warum die Layout-Berechnung aufwändig ist. Angenommen, ein Absatz wird höher, weil seine Schriftgröße zugenommen hat. Alles, was danach im Fluss kommt, muss möglicherweise nach unten verschoben werden, und die Höhe eines Containers kann sich ändern, was wiederum seine eigenen Kinderelemente beeinflussen kann. Eine scheinbar lokale Änderung kann sich über einen großen Bereich des Dokuments auswirken. Deshalb hilft es, beim Layoutdenken an die Lösung eines Netzwerks aus Abhängigkeiten statt daran, Kästchen auf eine Leinwand zu platzieren.

    Die Layout-Engine versucht, die Neuberechnung auf den betroffenen Unterbaum zu beschränken, doch wie viel Arbeit tatsächlich geleistet wird, hängt von der Art der Änderung sowie davon ab, wie sich die Elemente zueinander verhalten. Beispielsweise hat Inhalt in einem Container mit fester Größe im Allgemeinen weniger Auswirkungen als Inhalt in einem automatisch skalierenden Container.

    Die Invalidierung ist wichtiger als der Pipeline-Prozess

    Stellen Sie sich vor, ein Skript führt aus card.style.width = „500px“ aus. Der Browser weiß nun, dass seine vorherige Geometrie für diese Karte sowie möglicherweise auch für umliegende Elemente veraltet sein könnte. Bevor das nächste Frame angezeigt wird, muss er herausfinden, welche Schritte erneut ausgeführt werden müssen. In vereinfachter Form:

    Something changes
          │
          ▼
    What does it invalidate?
          │
          ├── Style
          ├── Layout
          ├── Paint
          └── Composite
    

    Die wichtigste Erkenntnis ist, dass nicht alle Änderungen den gesamten Pipeline-Prozess durchlaufen:

    • Eine neue Breite ändert die Geometrie, wodurch das Layout neu berechnet wird und der betroffene Bereich in der Regel erneut gemalt wird.
    • Eine neue Hintergrundfarbe ändert das Erscheinungsbild, ohne etwas zu verschieben; daher ist ein Neumalen erforderlich, doch in der Regel kein erneutes Layout.
    • Ein neuer transform-Wert kann oft mit weitaus weniger Aufwand angewendet werden – allerdings nur dann, wenn die Schichtentscheidungen des Browsers sowie die umgebenden Bedingungen es ermöglichen, die Aktualisierung in der Kompositierungsphase zu verarbeiten.

    Deshalb funktionieren allgemeine Ratschläge wie „vermeiden Sie Neuzeichnungen“ oder „verwenden Sie einfach die GPU“ in der Praxis nicht. Die nützliche diagnostische Frage lautet stets: Welche Aufgabe wurde durch diese spezielle Änderung ungültig? Unser Artikel zu was jede CSS-Änderung dem Browser kostet listet gängige Eigenschaften nach den Phasen auf, die sie in der Regel auslösen.

    Wie JavaScript die Render-Arbeit frühzeitig erzwingt

    Eine häufige Ursache für teure Render-Arbeiten durch Zufall ist das Mischen von Schreib- und Lesevorgängen bezüglich der Layouts. Die folgende Schleife legt einer jeden Karte eine Breite zu und fragt sofort nach der gemessenen Breite dieser Karte:

    for (const card of cards) {
      card.style.width = "500px";
      const width = card.getBoundingClientRect().width;
      console.log(width);
    }
    

    Es gibt nichts Falsches an getBoundingClientRect() selbst. Es handelt sich um eine normale, nützliche API. Der Aufwand entsteht durch die Reihenfolge der Operationen. Die Zuweisung des Styles lässt die Layout-Arbeit unerledigt, und bereits die nächste Zeile fordert einen Wert, der erst dann korrekt sein kann, wenn diese unerledigte Arbeit abgeschlossen ist. Der Browser hat keine andere Wahl, als Style- und Layout-Arbeiten synchron auszuführen, direkt dort, bevor er die Zahl zurückgibt. In einer Schleife entsteht das Phänomen, das als Layout Thrashing bezeichnet wird: Schreiben, Lesen, Schreiben, Lesen – mit einer erzwungenen Layout-Ausführung bei jeder Iteration.

    Durch eine Umstrukturierung des Codes, bei der alle Lesevorgänge zuerst und alle Schreibvorgänge danach stattfinden, wird die erzwungene Synchronisierung beseitigt:

    const widths = cards.map(card =>
      card.getBoundingClientRect().width
    );
    
    for (const card of cards) {
      card.style.width = "500px";
    }
    

    Nun werden die Messungen anhand eines einzigen, bereits gültigen Layouts vorgenommen, und die Schreibvorgänge werden dem Browser überlassen, um sie beim nächsten Render zusammen zu verarbeiten. Beachten Sie, dass die beiden Versionen nicht streng äquivalent sind: Die zweite version erfasst die Breiten vor der Änderung, weshalb diese Umordnung nur dann verwendet werden sollte, wenn dies tatsächlich erforderlich ist. Wenn ein Lesevorgang wirklich von einem vorherigen Schreibvorgang abhängt, sind die üblichen Methoden das Bündeln der Schreibvorgänge und das Planen der Lesevorgänge für den nächsten Frame – beispielsweise mit requestAnimationFrame.

    Die wichtigere Lektion lautet nicht „messen Sie niemals das Layout“. JavaScript und die Darstellung teilen denselben Thread sowie dieselben Daten, wodurch Ihr Code Abhängigkeiten schaffen kann, die den Browser dazu zwingen, sofort das zu tun, was er sonst effizient geplant und in Batches ausgeführt hätte. Es gibt auch andere subtile Auslöser für Layout-Probleme, die wir in live DOM Collections und Layout-Problemen erläutern.

    Paint ist eine separate Kostenstelle im Vergleich zum Layout

    Sobald die Geometrie bekannt ist, muss der Browser dennoch herausfinden, wie jede Region aussieht: Hintergründe, Rahmen, Textzeichen, Bilder, Schatten, Gradienten, Ausschnitte und andere Effekte. Eine einfache Methode, diese beiden Phasen voneinander zu trennen:

    • Das Layout bestimmt, wo sich die Elemente befinden und wie groß sie sind.
    • Paint bestimmt, wie diese Elemente aussehen.

    Das Ändern von background-color erfordert in der Regel keine neue Geometrie für das gesamte Dokument, während ein Ändern der Größe oder Position dies möglicherweise tut. Schließen Sie jedoch nicht daraus, dass das Zeichnen kostenlos ist. Das Zeichnen großer Flächen, aufwändige Effekte wie große verschwommene Schatten, häufige Änderungen des Inhalts sowie übermäßig umfassende Invalidationen können alle die Zeichnungszeit so in die Höhe treiben, dass dadurch ein Frame verzögert wird.

    Rasterisierung und Kompositing erzeugen den Endframe

    Paint generiert Zeichnungsanweisungen, nicht unbedingt die endgültigen Pixel. Diese Anweisungen werden anschließend in Pixeldaten rasterisiert, meist in Tiles aufgeteilt und mit dem Grafikstack der Plattform verarbeitet. Durch Kompositing werden die resultierenden Oberflächen oder Schichten schließlich zum Frame zusammengefügt, der auf dem Bildschirm angezeigt wird.

    Dies ist die Phase, in der transform und opacity ihren Ruf erlangen. Wenn die Bedingungen stimmen, ermöglicht die Animation dieser Eigenschaften es einem Element, sich zu bewegen, zu skalieren oder zu verblassen, ohne dass der Browser die Dokumentstruktur neu berechnen oder den Inhalt des Elements erneut zeichnen muss; stattdessen kombiniert er einfach die vorhandenen Ebenen neu.

    Die gängige Abkürzung „transform läuft auf der GPU, daher ist es immer schnell“ verdeckt jedoch die tatsächlichen Kosten:

    • Jede zusätzliche Ebene verbraucht Speicher, was bei Geräten mit schwacher Leistung von Bedeutung ist.
    • Der Inhalt muss dennoch zu einem Zeitpunkt gerastert werden, und die Rasterisierung kann aufwändig sein.
    • Ob ein Element seine eigene Ebene erhält, hängt von den Heuristiken des Browsers sowie vom Rendering-Kontext ab, sodass derselbe CSS in verschiedenen Situationen unterschiedlich funktionieren kann.

    Die grundlegende Regel ist einfacher als jede Eigenschaftsliste: eine Änderung, die weniger Inhalte ungültig macht, verlangt vom Engine weniger Neuberechnungen.

    Das Frame-Budget ist gemeinsam

    Ein 60Hz-Display erneuert sich alle etwa 16,67 Millisekunden, da 1000 / 60 ungefähr 16,67 ergibt. Bei 120Hz sinkt dieser Zeitraum auf etwa 8,33 Millisekunden.

    Dieser Zeitraum ist kein für Ihren Code reservierter CPU-Zeitraum. Darin muss der Browser die Ausführung von Skripten, die Berechnung von Stilen, das Layout, das Zeichnen, die Rasterisierung, das Kompositing, die Eingabebearbeitung, Netzwerkaufrufe sowie seine eigenen Verwaltungsaufgaben unterbringen. Daher kann eine einzige lange JavaScript-Aufgabe die Darstellung verzögern, selbst wenn die eigentliche Darstellungsarbeit eigentlich unkompliziert wäre.

    Deshalb existieren auch Metriken wie FCP (First Contentful Paint) und INP (Interaction to Next Paint) getrennt voneinander. Schnelles Anzeigen von Inhalten und schnelle Reaktion auf Eingaben sind miteinander verbundene Ziele, scheitern jedoch aus unterschiedlichen Gründen und müssen daher getrennt gemessen werden.

    Wo die UI erstellt wird, bestimmt die Renderkosten

    Noch bevor der Browser etwas rendern kann, hat eine architektonische Entscheidung bereits Einfluss darauf, wie viel Arbeit vorliegt: Wo wird die ursprüngliche UI erzeugt?

    Klientenseitiges Rendering

    Beim CSR sendet der Server eine minimale HTML-Struktur sowie den Anwendungscode, wobei das Gerät die meiste Arbeit übernimmt:

    Server
       │
       ├── HTML shell
       └── JavaScript + data
                │
                ▼
             Browser
                │
                ├── Parse JS
                ├── Execute JS
                ├── Fetch data
                ├── Build UI
                └── Render
    

    Serverseitiges Rendering

    Beim SSR finden das Abrufen von Daten, die Anwendungslogik sowie die Erstellung des HTMLs auf dem Server statt, bevor die Antwort gesendet wird:

    Request
       │
       ▼
    Server
       │
       ├── Fetch data
       ├── Execute application logic
       └── Generate HTML
                │
                ▼
             Browser
                │
                ▼
          Parse + Render
    

    SSR kann sinnvolle Markup-Strukturen schneller an den Browser übermitteln, während CSR die Berechnungen und Darstellung auf die Hardware des Benutzers verlagert. Im Allgemeinen ist keines der Verfahren besser. Die richtige Wahl hängt davon ab, wie komplex die Anwendung ist, welche Daten sie benötigt, wie gut die Antworten gekachtet werden können, welche Geräte die Nutzer haben und wie viel JavaScript letztendlich im Browser ausgeführt werden muss.

    Hydratierung: Bereit erscheinen bedeutet nicht, tatsächlich bereit zu sein

    Viele moderne Frameworks kombinieren diese beiden Ansätze durch die sogenannte Hydratierung. Der Server sendet HTML, das sofort angezeigt werden kann, und clientseitiges JavaScript startet anschließend die Anwendung sowie die für die Interaktion notwendigen Ereignishandler:

    Server HTML
         │
         ▼
    Browser renders content
         │
         ▼
    JavaScript loads
         │
         ▼
    Application initializes
         │
         ▼
    Hydration
         │
         ▼
    Interactive UI
    

    Die Folgen sind leicht übersehbar: eine Seite kann bereits viel früher als bereit erscheinen, bevor sie auf einen Klick reagieren kann. Die Initialisierung beinhaltet das Herunterladen, Parsen, Kompilieren und Ausführen vieler JavaScript-Dateien, die Initialisierung von Komponenten sowie das Einrichten von Listenern – und all das auf dem Hauptthread. Dadurch kann eine große Anwendung den Inhalt schneller anzeigen, verursacht aber unmittelbar danach einen starken Arbeitsaufkommen auf dem Hauptthread, genau zu dem Zeitpunkt, an dem die Benutzer zu tippen beginnen.

    Dieser Widerspruch erklärt die aktuelle Richtung der Rendering-Strategien: Weniger Client-JavaScript wird bereitgestellt, Bündel werden aufgeteilt und nur die Teile einer Seite aktiviert, die tatsächlich Interaktivität benötigen, anstatt den gesamten Baum auf einmal.

    Streaming ändert sich mit dem Start der Arbeit

    Klassisches SSR erstellt die vollständige Antwort, bevor etwas davon gesendet wird. Beim Streaming werden HTML-Teile schrittweise gesendet, sobald sie verfügbar sind:

    Server
      │
      ├── HTML shell ──────────→ Browser
      ├── Content ─────────────→ Browser
      ├── More content ────────→ Browser
      └── Remaining response ──→ Browser
    

    Der Browser kann die ersten Datenblöcke bereits parsen und darstellen, während der Server noch den Rest erzeugt. Streaming macht die Gesamtdarstellung nicht günstiger. Was sich ändert, ist der Zeitpunkt, zu dem mit der nützlichen Verarbeitung begonnen werden kann, was den Weg von der Navigation bis zum sichtbaren Inhalt verkürzen kann. Progressive Hydratation wendet denselben Ansatz auf der Client-Seite an, indem sie die Initialisierung über Zeit verteilt statt die gesamte Anwendung auf einmal zu aktivieren.

    Beobachten Sie den Ablauf statt ihn auswendig zu lernen

    Der schnellste Weg, ein Gespür dafür zu entwickeln, ist es, den Browser direkt zu beobachten. Öffnen Sie das Performance-Panel in Chrome DevTools, nehmen Sie eine Interaktion auf und prüfen Sie, was abläuft, wenn sich die Benutzeroberfläche ändert. Legen Sie anschließend absichtlich eine gezwungene Anordnung fest, indem Sie sofort eine Breite festlegen und die Geometrie abrufen:

    element.style.width = "600px";
    element.getBoundingClientRect();
    

    Nehmen Sie erneut eine Aufnahme mit einem Update auf, das nur eine Transformation ändert:

    element.style.transform = "translateX(100px)";
    

    Ziel der Übung ist es nicht, eine Eigenschaft als gut und die andere als schlecht zu bezeichnen. Bearbeiten Sie für jede Aufnahme die Fragen nacheinander:

    • Was wurde bezüglich des Styles neu berechnet?
    • Wurde das Layout ausgeführt, und wurde dies durch ein Skript erzwungen?
    • Wurde ein Bereich neu gemalt?
    • Fand eine Rasterisierung statt?
    • Könnte die Aktualisierung vollständig vom Kompositor erledigt werden?

    Sobald Sie die Spuren auf diese Weise lesen, ist das Performance-Panel nicht mehr nur ein Notfall-Debugging-Werkzeug, sondern zeigt Ihnen, wie der Engine auf jede Zeile Ihres Codes reagiert.

    Jenseits des sechsstufigen Diagramms

    Der klassische Ablauf ist dennoch zu kennen:

    HTML → DOM → CSSOM → Layout → Paint → Composite
    

    Das eigentliche System ist komplexer. Der Engine hält stets mehrere miteinander verknüpfte Darstellungsformen synchron – vom Elementbaum und den berechneten Stilen über die Box-Geometrie und Zeichnungsanweisungen bis hin zu Rasterfliesen und komponierten Ebenen. Eine Änderung zwingt selten dazu, alle diese Elemente neu zu erstellen. Der Engine ermittelt, was weiterhin gültig ist, markiert das Veraltete und nutzt alles, was möglich ist, erneut.

    Haupterkenntnisse

    • Ersetzen Sie die Frage „Welche Optimierung soll ich anwenden?“ durch „Was hat sich geändert, was wurde dadurch ungültig und warum musste der Browser diese Arbeit erneut durchführen?“
    • Die Layout-Verarbeitung ist ein Abhängigkeitsproblem; scheinbar lokale Änderungen können sich im gesamten Dokument auswirken.
    • Mischt man Lese- und Schreibvorgänge, wird das Layout synchronisiert; trennen Sie sie voneinander oder verschieben Sie die Lesevorgänge auf den nächsten Frame.
  • Bemalen und Kompositing haben ihre eigenen Kosten; transform und opacity sind nur dann günstig, wenn Schichten und Bedingungen zusammenwirken.
  • Der Ressourcenrahmen wird mit Skripten und der Eingabeverarbeitung geteilt, sodass lange Aufgaben auch dann zu Problemen führen, wenn die Renderung kostengünstig ist.
  • CSR, SSR, Hydratierung und Streaming verschieben hauptsächlich die Ausführung von Aufgaben in Zeit und Ort; beurteilen Sie sie danach, wie viel Arbeit auf dem Hauptthread am Gerät des Benutzers anfällt und zu welchem Zeitpunkt.