Funktionsweise eines Renderer-Prozesses
Dies ist Teil 3 einer vierteiligen Blogreihe zur Funktionsweise von Browsern. Zuvor haben wir die mehrstufige Architektur und den Navigationsfluss behandelt. In diesem Beitrag sehen wir uns an, was im Rendering-Prozess passiert.
Der Rendererprozess wirkt sich auf viele Aspekte der Webleistung aus. Da beim Rendering-Prozess viel passiert, ist dieser Beitrag nur ein allgemeiner Überblick. Wenn Sie mehr darüber erfahren möchten, finden Sie im Abschnitt zur Leistung der Webgrundlagen viele weitere Ressourcen.
Renderer-Prozesse verarbeiten Webinhalte
Der Renderer-Prozess ist für alles verantwortlich, was auf einem Tab passiert. In einem Renderer-Prozess verarbeitet der Haupt-Thread den Großteil des Codes, den Sie an den Nutzer senden. Wenn Sie einen Webworker oder Service Worker verwenden, werden Teile Ihres JavaScript manchmal von Worker-Threads verarbeitet. Compositor- und Raster-Threads werden ebenfalls in einem Renderer-Prozess ausgeführt, um eine Seite effizient und reibungslos zu rendern.
Die Hauptaufgabe des Rendering-Prozesses besteht darin, HTML, CSS und JavaScript in eine Webseite umzuwandeln, mit der Nutzer interagieren können.

Parsen
Aufbau eines DOM
Wenn der Renderer-Prozess eine Commit-Nachricht für eine Navigation empfängt und HTML-Daten erhält, beginnt der Haupt-Thread, den Textstring (HTML) zu parsen und in ein Document Object Model (DOM) umzuwandeln.
Das DOM ist die interne Darstellung der Seite in einem Browser sowie die Datenstruktur und API, mit der Webentwickler über JavaScript interagieren können.
Das Parsen eines HTML-Dokuments in ein DOM wird durch den HTML-Standard definiert. Möglicherweise haben Sie bemerkt, dass beim Eingeben von HTML in einen Browser nie ein Fehler auftritt. Ein fehlendes schließendes </p>
-Tag ist beispielsweise gültiges HTML. Falsche Markierungen wie Hi! <b>I'm <i>Chrome</b>!</i>
(b-Tag wird vor dem i-Tag geschlossen) werden so behandelt, als hätten Sie Hi! <b>I'm <i>Chrome</i></b><i>!</i>
geschrieben. Das liegt daran, dass die HTML-Spezifikation so konzipiert ist, dass diese Fehler möglichst reibungslos verarbeitet werden. Wenn Sie wissen möchten, wie das funktioniert, lesen Sie den Abschnitt Einführung in die Fehlerbehandlung und ungewöhnliche Fälle im Parser in der HTML-Spezifikation.
Laden von Unterressourcen
Eine Website verwendet in der Regel externe Ressourcen wie Bilder, CSS und JavaScript. Diese Dateien müssen aus dem Netzwerk oder Cache geladen werden. Der Haupt-Thread könnte sie einzeln anfordern, wenn er sie beim Parsen zum Erstellen eines DOM findet. Zur Beschleunigung wird der „Preload-Scanner“ jedoch gleichzeitig ausgeführt.
Wenn sich im HTML-Dokument Elemente wie <img>
oder <link>
befinden, sieht sich der Preloader-Scanner die vom HTML-Parser generierten Tokens an und sendet Anfragen an den Netzwerk-Thread im Browserprozess.

JavaScript kann das Parsen blockieren
Wenn der HTML-Parser ein <script>
-Tag findet, wird das Parsen des HTML-Dokuments pausiert und der JavaScript-Code muss geladen, geparst und ausgeführt werden. Warum? Weil JavaScript die Form des Dokuments mithilfe von Elementen wie document.write()
ändern kann, wodurch sich die gesamte DOM-Struktur ändert. In der Übersicht über das Parsemodell in der HTML-Spezifikation finden Sie ein schönes Diagramm. Deshalb muss der HTML-Parser warten, bis JavaScript ausgeführt wurde, bevor er mit dem Parsen des HTML-Dokuments fortfahren kann. Wenn Sie wissen möchten, was bei der JavaScript-Ausführung passiert, hat das V8-Team dazu Vorträge und Blogbeiträge veröffentlicht.
Browser angeben, wie Ressourcen geladen werden sollen
Es gibt viele Möglichkeiten, wie Webentwickler dem Browser Hinweise senden können, um Ressourcen effizient zu laden.
Wenn in Ihrem JavaScript kein document.write()
verwendet wird, können Sie dem <script>
-Tag das Attribut async
oder defer
hinzufügen. Der Browser lädt und führt den JavaScript-Code dann asynchron aus und blockiert das Parsen nicht. Sie können auch das JavaScript-Modul verwenden, wenn dies geeignet ist. Mit <link rel="preload">
können Sie den Browser darüber informieren, dass die Ressource für die aktuelle Navigation unbedingt erforderlich ist und Sie sie so schnell wie möglich herunterladen möchten. Weitere Informationen finden Sie unter Ressourcenpriorisierung – den Browser als Helfer nutzen.
Stilberechnung
Ein DOM reicht nicht aus, um zu wissen, wie die Seite aussehen würde, da wir Seitenelemente in CSS stylen können. Der Haupt-Thread analysiert CSS und ermittelt den berechneten Stil für jeden DOM-Knoten. Hier erfahren Sie, welche Art von Stil auf jedes Element basierend auf CSS-Selektoren angewendet wird. Sie finden diese Informationen in den DevTools im Bereich computed
.

Auch wenn Sie kein CSS angeben, hat jeder DOM-Knoten einen berechneten Stil. Das <h1>
-Tag wird größer als das <h2>
-Tag angezeigt und für jedes Element werden Ränder definiert. Das liegt daran, dass der Browser ein Standard-Stylesheet hat. Wenn Sie wissen möchten, wie das Standard-CSS von Chrome aussieht, finden Sie hier den Quellcode.
Layout
Der Rendering-Prozess kennt jetzt die Struktur eines Dokuments und die Stile für die einzelnen Knoten. Das reicht jedoch nicht aus, um eine Seite zu rendern. Stellen Sie sich vor, Sie versuchen, einem Freund ein Gemälde am Telefon zu beschreiben. „Es gibt einen großen roten Kreis und ein kleines blaues Quadrat“ sind nicht genügend Informationen, damit Ihr Freund weiß, wie genau das Gemälde aussehen würde.

Das Layout ist ein Prozess, um die Geometrie von Elementen zu finden. Der Haupt-Thread durchläuft das DOM und berechnete Stile und erstellt den Layout-Baum mit Informationen wie X‑ und Y‑Koordinaten und Begrenzungsboxgrößen. Das Layout-Bäumchen kann eine ähnliche Struktur wie das DOM-Bäumchen haben, enthält aber nur Informationen zu den Elementen, die auf der Seite sichtbar sind. Wenn display: none
angewendet wird, ist dieses Element nicht Teil des Layoutbaums. Ein Element mit visibility: hidden
ist jedoch im Layoutbaum enthalten. Wenn ein Pseudo-Element mit Inhalten wie p::before{content:"Hi!"}
angewendet wird, wird es ebenfalls in den Layoutbaum aufgenommen, auch wenn es nicht im DOM enthalten ist.

Das Layout einer Seite zu bestimmen, ist eine anspruchsvolle Aufgabe. Selbst beim einfachsten Seitenlayout wie einem Blockfluss von oben nach unten muss berücksichtigt werden, wie groß die Schrift ist und wo Zeilenumbrüche eingefügt werden, da sich dies auf die Größe und Form eines Absatzes auswirkt, was wiederum Auswirkungen darauf hat, wo der nächste Absatz platziert werden muss.
Mit CSS können Sie Elemente an eine Seite floaten, Überlaufelemente maskieren und die Schreibrichtung ändern. Wie Sie sich vorstellen können, ist diese Layoutphase eine große Herausforderung. Bei Chrome arbeitet ein ganzes Team von Entwicklern am Layout. Wenn Sie mehr über ihre Arbeit erfahren möchten, können Sie sich einige Vorträge von der BlinkOn-Konferenz ansehen.
Farben

Ein DOM, ein Stil und ein Layout reichen nicht aus, um eine Seite zu rendern. Angenommen, Sie möchten ein Gemälde reproduzieren. Sie kennen die Größe, Form und Position der Elemente, müssen aber noch entscheiden, in welcher Reihenfolge Sie sie malen.
Beispielsweise kann z-index
für bestimmte Elemente festgelegt sein. In diesem Fall führt das Malen in der Reihenfolge der in HTML geschriebenen Elemente zu einem falschen Rendering.

In diesem Schritt der Paint-Phase durchläuft der Hauptthread den Layoutbaum, um Paint-Einträg zu erstellen. Ein Paint-Eintrag ist eine Notiz zum Malvorgang, z. B. „Zuerst Hintergrund, dann Text, dann Rechteck“. Wenn Sie mit JavaScript bereits auf das Element <canvas>
gezeichnet haben, ist dieser Vorgang möglicherweise bereits bekannt.

Aktualisierung der Rendering-Pipeline ist kostspielig
Das Wichtigste bei der Rendering-Pipeline ist, dass in jedem Schritt das Ergebnis des vorherigen Vorgangs verwendet wird, um neue Daten zu erstellen. Wenn sich beispielsweise etwas im Layoutbaum ändert, muss die Paint-Reihenfolge für die betroffenen Teile des Dokuments neu generiert werden.
Wenn Sie Elemente animieren, muss der Browser diese Vorgänge zwischen jedem Frame ausführen. Die meisten Displays aktualisieren das Display 60 Mal pro Sekunde (60 fps). Die Animation erscheint für das menschliche Auge flüssig, wenn Sie Objekte in jedem Frame über den Bildschirm bewegen. Wenn die Animation jedoch die Frames dazwischen überspringt, wirkt die Seite unruhig.

Selbst wenn Ihre Rendering-Vorgänge mit der Bildschirmaktualisierung Schritt halten, werden diese Berechnungen im Hauptthread ausgeführt. Das bedeutet, dass er blockiert werden kann, wenn in Ihrer Anwendung JavaScript ausgeführt wird.

Sie können JavaScript-Vorgänge in kleine Teile aufteilen und mit requestAnimationFrame()
festlegen, dass sie in jedem Frame ausgeführt werden. Weitere Informationen zu diesem Thema finden Sie unter JavaScript-Ausführung optimieren. Sie können Ihr JavaScript auch in Webworkern ausführen, um das Blockieren des Hauptthreads zu vermeiden.

Compositing
Wie würden Sie eine Seite zeichnen?
Nachdem der Browser die Struktur des Dokuments, den Stil der einzelnen Elemente, die Geometrie der Seite und die Malreihenfolge kennt, wie wird eine Seite gezeichnet? Diese Informationen in Pixel auf dem Bildschirm umzuwandeln, wird als Rasterung bezeichnet.
Eine naive Lösung wäre, Teile innerhalb des Darstellungsbereichs zu rastern. Wenn ein Nutzer auf der Seite scrollt, verschieben Sie den gerasterten Frame und füllen Sie die fehlenden Teile durch weiteres Rastern aus. So wurde die Rasterung in Chrome bei der Erstveröffentlichung durchgeführt. Moderne Browser führen jedoch einen ausgefeilteren Prozess namens Rendering aus.
Was ist Compositing?
Beim Compositing werden Teile einer Seite in Ebenen unterteilt, separat gerastert und in einem separaten Thread (Compositor-Thread) als Seite zusammengesetzt. Wenn der Nutzer scrollt, muss nur ein neuer Frame zusammengesetzt werden, da die Ebenen bereits gerastert sind. Auf die gleiche Weise können Sie Animationen erstellen, indem Sie Ebenen verschieben und einen neuen Frame zusammensetzen.
Im Bereich Ebenen in den DevTools sehen Sie, wie Ihre Website in Ebenen unterteilt ist.
In Ebenen unterteilen
Um herauszufinden, welche Elemente in welchen Ebenen sein müssen, durchläuft der Haupt-Thread den Layout-Baum, um den Ebenen-Baum zu erstellen. Dieser Teil wird im DevTools-Steuerfeld „Leistung“ als „Ebenen-Baum aktualisieren“ bezeichnet. Wenn bestimmte Bereiche einer Seite, die eine separate Ebene sein sollten (z. B. ein ausziehbares Seitenmenü), keine erhalten, können Sie dem Browser mit dem Attribut will-change
in CSS einen Hinweis geben.

Sie könnten versucht sein, jedem Element Ebenen zuzuweisen. Das Zusammenführen über eine zu große Anzahl von Ebenen kann jedoch zu einer langsameren Ausführung führen als das Rastern kleiner Teile einer Seite pro Frame. Daher ist es wichtig, die Renderingleistung Ihrer Anwendung zu messen. Weitere Informationen zu diesem Thema finden Sie unter Nur für den Compositor bestimmte Properties verwenden und die Anzahl der Ebenen verwalten.
Raster und Komposition außerhalb des Hauptthreads
Sobald der Ebenenstrukturbaum erstellt und die Malanordnungen festgelegt wurden, übergibt der Hauptthread diese Informationen an den Kompositionsleiter. Der Kompositions-Thread rastert dann jede Ebene. Eine Ebene kann so groß sein wie die gesamte Länge einer Seite. Daher teilt der Compositor-Thread sie in Kacheln auf und sendet jede Kachel an Raster-Threads. Raster-Threads rastern jede Kachel und speichern sie im GPU-Speicher.

Der Compositor-Thread kann verschiedene Raster-Threads priorisieren, damit Elemente innerhalb des Viewports (oder in der Nähe) zuerst gerastert werden. Eine Ebene hat auch mehrere Kachelungen für verschiedene Auflösungen, um z. B. Zoomaktionen zu verarbeiten.
Sobald die Kacheln gerastert sind, sammelt der Compositor-Thread Kachelninformationen, die als Draw Quads bezeichnet werden, um einen Compositor-Frame zu erstellen.
Quadrate zeichnen | Enthält Informationen wie den Speicherort der Kachel und die Position auf der Seite, an der die Kachel unter Berücksichtigung der Seitenkomposition gezeichnet werden soll. |
Compositor-Frame | Eine Sammlung von Draw Quads, die einen Frame einer Seite darstellt. |
Ein Compose-Frame wird dann über IPC an den Browserprozess gesendet. An dieser Stelle kann über den UI-Thread ein weiterer Compose-Frame für die Änderung der Browser-Benutzeroberfläche oder über andere Renderer-Prozesse für Erweiterungen hinzugefügt werden. Diese Frames werden an die GPU gesendet, um sie auf einem Bildschirm anzuzeigen. Wenn ein Scroll-Ereignis eintritt, erstellt der Compositor-Thread einen weiteren Compositor-Frame, der an die GPU gesendet wird.

Der Vorteil des Compositings besteht darin, dass der Hauptthread nicht beteiligt ist. Der Compositor-Thread muss nicht auf die Stilberechnung oder die JavaScript-Ausführung warten. Deshalb gilt nur das Zusammenführen von Animationen als die beste Lösung für eine flüssige Leistung. Wenn das Layout oder die Darstellung neu berechnet werden muss, muss der Hauptthread beteiligt sein.
Zusammenfassung
In diesem Beitrag haben wir uns die Rendering-Pipeline vom Parsen bis zum Compositing angesehen. Ich hoffe, dass Sie jetzt mehr über die Leistungsoptimierung einer Website erfahren haben.
Im nächsten und letzten Beitrag dieser Reihe sehen wir uns den Compositor-Thread genauer an und erfahren, was passiert, wenn Nutzereingaben wie mouse move
und click
eingehen.
Hat Ihnen der Beitrag gefallen? Wenn du Fragen oder Vorschläge für den nächsten Beitrag hast, kannst du sie mir unten in den Kommentaren oder auf Twitter unter @kosamari senden.