Ein Blick auf moderne Webbrowser (Teil 3)

Mariko Kosaka

Innere Funktionsweise eines Renderer-Prozesses

Dies ist Teil 3 einer vierteiligen Blogreihe, in der es um die Funktionsweise von Browsern geht. Zuvor haben wir die Multi-Prozess-Architektur und den Navigationsablauf behandelt. In diesem Post schauen wir uns an, was im Renderer-Prozess passiert.

Der Renderer-Prozess beeinflusst viele Aspekte der Webleistung. Da im Rendererprozess viel los ist, bietet dieser Beitrag nur einen allgemeinen Überblick. Ausführlichere Informationen finden Sie im Abschnitt zur Leistung in Web Fundamentals.

Renderer-Prozesse verarbeiten Webinhalte

Der Renderer-Prozess ist für alles verantwortlich, was innerhalb eines Tabs geschieht. In einem Renderer-Prozess verarbeitet der Hauptthread den Großteil des Codes, den Sie an den Nutzer senden. Manchmal werden Teile Ihres JavaScript-Codes von Worker-Threads verarbeitet, wenn Sie einen Web Worker oder einen Service Worker verwenden. Compositor- und Raster-Threads werden auch innerhalb von Renderer-Prozessen ausgeführt, um eine Seite effizient und reibungslos zu rendern.

Die Hauptaufgabe des Renderer-Prozesses besteht darin, HTML, CSS und JavaScript in eine Webseite umzuwandeln, mit der der Nutzer interagieren kann.

Renderer-Prozess
Abbildung 1: Renderer-Prozess mit einem Hauptthread, Worker-Threads, einem zusammengesetzten Thread und einem Rasterthread darin

Parsen

Aufbau eines DOMs

Wenn der Renderer-Prozess eine Commit-Nachricht für eine Navigation empfängt und HTML-Daten empfangen kann, beginnt der Hauptthread, den Textstring (HTML) zu parsen und ihn in ein Document-Object-Model (DOM) umzuwandeln.

Das DOM ist die interne Darstellung der Seite im Browser sowie die Datenstruktur und die API, mit denen Webentwickler über JavaScript interagieren können.

Das Parsen eines HTML-Dokuments in ein DOM ist durch den HTML-Standard definiert. Wie Sie vielleicht bemerkt haben, wird beim Einspeisen von HTML in einen Browser nie ein Fehler ausgegeben. Wenn beispielsweise das schließende </p>-Tag fehlt, handelt es sich um einen gültigen HTML-Code. Fehlerhaftes Markup wie Hi! <b>I'm <i>Chrome</b>!</i> (b-Tag wird vor dem i-Tag geschlossen) wird so behandelt, als hättest du Hi! <b>I'm <i>Chrome</i></b><i>!</i> geschrieben. Das liegt daran, dass die HTML-Spezifikation darauf ausgelegt ist, diese Fehler korrekt zu behandeln. Wenn Sie wissen möchten, wie dies funktioniert, lesen Sie den Abschnitt An intro Introduction to Error Handling and Free Cases in the Parser (Einführung zur Fehlerbehandlung und ungewöhnlichen Fälle im Parser) der HTML-Spezifikation.

Unterressource wird geladen

Eine Website verwendet normalerweise externe Ressourcen wie Bilder, CSS und JavaScript. Diese Dateien müssen aus dem Netzwerk oder Cache geladen werden. Der Hauptthread könnte sie nacheinander anfordern, wenn er sie beim Parsen findet, um ein DOM zu erstellen. Um den Vorgang zu beschleunigen, wird jedoch „Preload Scanner“ gleichzeitig ausgeführt. Wenn das HTML-Dokument Elemente wie <img> oder <link> enthält, prüft der Preload-Scanner Tokens, die vom HTML-Parser generiert wurden, und sendet Anfragen an den Netzwerk-Thread im Browserprozess.

DOM
Abbildung 2: Der Hauptthread beim Parsen von HTML und Erstellen eines DOM-Baums

JavaScript kann das Parsing blockieren

Wenn der HTML-Parser ein <script>-Tag findet, wird das Parsen des HTML-Dokuments angehalten und der JavaScript-Code muss geladen, geparst und ausgeführt. Warum? Weil JavaScript die Form des Dokuments mithilfe von Dingen wie document.write() ändern kann, wodurch die gesamte DOM-Struktur geändert wird (Übersicht über das Parsing-Modell in der HTML-Spezifikation enthält ein schönes Diagramm). Aus diesem Grund muss der HTML-Parser warten, bis JavaScript ausgeführt wird, bevor er das Parsen des HTML-Dokuments fortsetzen kann. Wenn du wissen möchtest, was bei der JavaScript-Ausführung passiert, findest du im V8-Team Vorträge und Blogposts dazu.

Hinweis für den Browser, wie Ressourcen geladen werden sollen

Es gibt viele Möglichkeiten, wie Webentwickler Hinweise an den Browser senden können, um Ressourcen ordnungsgemäß zu laden. Wenn dein JavaScript document.write() nicht verwendet, kannst du dem <script>-Tag das Attribut async oder defer hinzufügen. Der Browser lädt dann den JavaScript-Code und führt ihn asynchron aus. Das Parsen wird nicht blockiert. Sie können auch ein JavaScript-Modul verwenden, wenn dies geeignet ist. Mit <link rel="preload"> können Sie den Browser darüber informieren, dass die Ressource auf jeden Fall für die aktuelle Navigation erforderlich ist und Sie sie so schnell wie möglich herunterladen möchten. Weitere Informationen dazu finden Sie unter Ressourcenpriorisierung – Der Browser unterstützt Sie.

Stilberechnung

Ein DOM reicht nicht aus, um zu wissen, wie die Seite aussehen würde, da wir Seitenelemente in CSS gestalten können. Der Hauptthread parst CSS und bestimmt den berechneten Stil für jeden DOM-Knoten. Hier erfahren Sie, welche Art von Stil basierend auf CSS-Selektoren auf die einzelnen Elemente angewendet wird. Du findest diese Informationen im Abschnitt computed der Entwicklertools.

Berechneter Stil
Abbildung 3: Der Hauptthread parst CSS, um den berechneten Stil hinzuzufügen

Auch wenn Sie kein CSS angeben, verfügt jeder DOM-Knoten über einen berechneten Stil. Das <h1>-Tag wird größer als das <h2>-Tag angezeigt und die Ränder sind für jedes Element definiert. Das liegt daran, dass der Browser ein Standard-Style-Sheet verwendet. Den Standard-CSS-Code von Chrome können Sie hier sehen.

Layout

Jetzt kennt der Renderer-Prozess die Struktur eines Dokuments und die Stile für die einzelnen Knoten. Dies reicht jedoch nicht aus, um eine Seite zu rendern. Stellen Sie sich vor, Sie versuchen, Ihrem Freund ein Gemälde per Smartphone zu beschreiben. „Es gibt einen großen roten Kreis und ein kleines blaues Quadrat“ reicht nicht aus, damit Ihr Freund weiß, wie das Gemälde genau aussehen wird.

Spiel eines menschlichen Faxgeräts
Abbildung 4: Eine Person steht vor einem Gemälde. Die Telefonleitung ist mit der anderen Person verbunden.

Das Layout ist ein Prozess, bei dem die Geometrie von Elementen ermittelt wird. Der Hauptthread geht durch das DOM und die berechneten Stile und erstellt den Layoutbaum mit Informationen wie x-y-Koordinaten und Begrenzungsrahmengrößen. Die Layoutstruktur ähnelt der DOM-Struktur, enthält aber nur Informationen zu dem, was auf der Seite sichtbar ist. Wenn display: none angewendet wird, ist dieses Element nicht Teil des Layoutbaums. Ein Element mit visibility: hidden befindet sich jedoch im Layoutbaum. Wenn ein Pseudoelement mit Inhalten wie p::before{content:"Hi!"} angewendet wird, ist es ebenfalls in der Layoutstruktur enthalten, auch wenn sich dieses nicht im DOM befindet.

Layout

Die Festlegung des Layouts einer Seite ist eine schwierige Aufgabe. Selbst beim einfachsten Seitenlayout, wie bei einem Blockfluss von oben nach unten, muss berücksichtigt werden, wie groß die Schriftart ist und an welcher Stelle sie umgebrochen werden sollen, da diese sich auf die Größe und Form eines Absatzes auswirken und sich dann darauf auswirken, wo der folgende Absatz platziert werden muss.

CSS kann dazu führen, dass ein Element auf einer Seite schwebt, das Überlaufelement maskiert und die Schreibrichtung geändert wird. Stellen Sie sich vor, diese Layout-Phase hat eine große Aufgabe. In Chrome arbeitet ein ganzes Team von Entwicklern an dem Layout. Wenn Sie die Details ihrer Arbeit sehen möchten, werden wenige Vorträge von BlinkOn Conference aufgezeichnet und sind sehr interessant für Sie.

Farben

Zeichenspiel
Abbildung 7: Eine Person vor einem Leinwand, die einen Pinsel hält und sich fragt, ob sie zuerst einen Kreis oder zuerst ein Quadrat zeichnen soll

Ein DOM, ein Stil und ein Layout reichen immer noch nicht aus, um eine Seite zu rendern. Angenommen, Sie versuchen, ein Gemälde zu reproduzieren. Sie kennen die Größe, Form und Position der Elemente, müssen aber dennoch beurteilen, in welcher Reihenfolge sie gezeichnet werden.

Beispielsweise kann z-index für bestimmte Elemente festgelegt sein. In diesem Fall führt das Malen in der Reihenfolge der im HTML-Code geschriebenen Elemente zu einem falschen Rendering.

Z-Index fehlgeschlagen
Abbildung 8: Seitenelemente, die in der Reihenfolge eines HTML-Markups erscheinen, was zu einem falsch gerenderten Bild führt, da der Z-Index nicht berücksichtigt wurde

Bei diesem Schritt des Zeichnens durchläuft der Hauptthread den Layoutbaum, um Paint-Datensätze zu erstellen. Paint Record ist eine Notiz eines Malprozesses wie „zuerst Hintergrund, dann Text, dann Rechteck“. Wenn Sie mit JavaScript auf ein <canvas>-Element gezeichnet haben, ist Ihnen dieser Vorgang möglicherweise bekannt.

Paint-Rekorde
Abbildung 9: Der Hauptthread, der den Layoutbaum durchläuft und Paint-Datensätze generiert

Das Aktualisieren der Rendering-Pipeline ist kostspielig

Abbildung 10: DOM+Stil, Layout und Farbstruktur in der Reihenfolge, in der sie generiert wurden

Das Wichtigste bei der Rendering-Pipeline ist, dass bei jedem Schritt das Ergebnis des vorherigen Vorgangs verwendet wird, um neue Daten zu erstellen. Wenn sich beispielsweise etwas in der Layoutstruktur ä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. Bei den meisten Displays wird der Bildschirm 60-mal pro Sekunde (60 fps) aktualisiert. Die Animation wird für menschliche Augen flüssig dargestellt, wenn Sie Dinge bei jedem Frame über den Bildschirm bewegen. Wenn bei der Animation jedoch die dazwischen liegenden Frames fehlen, wird die Seite als instabil dargestellt.

Jage-Verzögerung durch fehlende Frames
Abbildung 11: Animationsframes auf einer Zeitachse

Selbst wenn Ihre Renderingvorgänge mit der Bildschirmaktualisierung Schritt halten, werden diese Berechnungen im Hauptthread ausgeführt. Das bedeutet, dass er blockiert wird, wenn Ihre Anwendung JavaScript ausführt.

Jage-Verzögerung von JavaScript
Abbildung 12: Animationsframes auf einer Zeitachse, bei denen ein Frame durch JavaScript blockiert wird

Sie können den JavaScript-Vorgang mit requestAnimationFrame() in kleine Teile aufteilen und die Ausführung für jeden Frame planen. Weitere Informationen zu diesem Thema finden Sie unter JavaScript-Ausführung optimieren. Sie können Ihren JavaScript-Code auch in Web Workers ausführen, um das Blockieren des Hauptthreads zu vermeiden.

Animationsframe anfordern
Abbildung 13: Kleinere JavaScript-Blöcke auf einer Zeitachse mit Animationsframe

Compositing

Wie würden Sie eine Seite zeichnen?

Abbildung 14: Animation eines naiven Rasterprozesses

Der Browser kennt nun die Struktur des Dokuments, den Stil jedes Elements, die Geometrie der Seite und die Farbreihenfolge. Wie zeichnet er eine Seite? Das Umwandeln dieser Informationen in Pixel auf dem Bildschirm wird als Rasterung bezeichnet.

Eine einfache Möglichkeit, dies zu bewältigen, wäre das Rastern von Teilen innerhalb des Darstellungsbereichs. Wenn ein Nutzer auf der Seite scrollt, verschieben Sie den Rasterframe und füllen die fehlenden Teile durch weitere Rasterung aus. So wurde die Rasterung in Chrome in der ersten Version beschrieben. In modernen Browsern wird jedoch ein komplexerer Prozess ausgeführt, der als Compositing bezeichnet wird.

Was ist Compositing?

Abbildung 15: Animation des Zusammensetzungsprozesses

Beim Compositing handelt es sich um eine Technik, mit der Teile einer Seite in Ebenen aufgeteilt, getrennt gerastert und als Seite in einem separaten Thread zusammengefasst werden, der als Compositor-Thread bezeichnet wird. Da die Ebenen bereits gerastert sind, muss beim Scrollen nur ein neuer Frame zusammengesetzt werden. Animationen lassen sich auf die gleiche Weise durchführen, indem Sie Ebenen verschieben und einen neuen Frame zusammensetzen.

Im Bereich Layers (Ebenen) kannst du in den Entwicklertools sehen, wie deine Website in Ebenen unterteilt ist.

In Ebenen unterteilen

Um herauszufinden, welche Elemente in welchen Ebenen vorhanden sein müssen, geht der Hauptthread durch die Layoutstruktur, um den Ebenenbaum zu erstellen. Im Leistungssteuerfeld der Entwicklertools heißt dieser Teil „Ebenenstruktur aktualisieren“. Wenn bestimmte Bereiche einer Seite, die eine separate Ebene sein sollen (z. B. ein seitliches Menü, das eingeschoben werden soll), nicht angezeigt werden, können Sie dem Browser mithilfe des will-change-Attributs in CSS einen Hinweis senden.

Ebenenbaum
Abbildung 16: Der Hauptthread, der einen Layoutbaum führt und den Ebenenbaum erzeugt

Es mag verlockend sein, jedem Element Ebenen zuzuweisen, aber das Zusammensetzen über eine übermäßige Anzahl von Ebenen kann den Vorgang verlangsamen als das Rastern kleiner Teile einer Seite mit jedem Frame. Daher ist es wichtig, dass Sie die Rendering-Leistung Ihrer Anwendung messen. Weitere Informationen zu diesem Thema finden Sie unter Nur Compositor-Eigenschaften beibehalten und Ebenenanzahl verwalten.

Rasterung und Zusammensetzung aus dem Hauptthread

Nachdem die Ebenenstruktur erstellt und die Darstellungsreihenfolge festgelegt wurde, übergibt der Hauptthread diese Informationen an den Compositor-Thread. Der Compositor-Thread rastert dann jede Ebene. Eine Ebene kann so groß wie die gesamte Länge einer Seite sein. Daher teilt der Compositor-Thread diese in Kacheln auf und sendet jede Kachel an Raster-Threads. Rasterthreads rastern jede Kachel und speichern sie im GPU-Speicher.

Raster
Abbildung 17: Raster-Threads, die die Bitmap der Kacheln erstellen und an GPU senden

Der Compositor-Thread kann verschiedene Raster-Threads priorisieren, sodass Elemente im Darstellungsbereich (oder in der Nähe) zuerst gerastert werden können. Eine Ebene hat auch mehrere Tilings für verschiedene Auflösungen, um Dinge wie Zoomaktionen zu verarbeiten.

Sobald die Kacheln gerastert wurden, erfasst der Compositor-Thread Kachelinformationen, die als Dreh-Quads bezeichnet werden, um einen Compositor-Frame zu erstellen.

Vierecke zeichnen Enthält Informationen wie den Speicherort der Kachel im Speicher und wo auf der Seite unter Berücksichtigung der Seitenerstellung die Kachel gezeichnet werden soll.
Compositor-Frame Mehrere Zeichenquadrate, die einen Frame einer Seite darstellen.

Ein zusammengesetzter Frame wird dann über IPC an den Browserprozess gesendet. An dieser Stelle kann ein weiterer zusammengesetzter Frame aus dem UI-Thread für die Änderung der Browser-UI oder aus anderen Renderer-Prozessen für Erweiterungen hinzugefügt werden. Diese zusammengesetzten Frames werden an die GPU gesendet, um sie auf einem Bildschirm anzuzeigen. Wenn ein Scroll-Ereignis eingeht, erstellt der Compositor-Thread einen weiteren zusammengesetzten Frame, der an die GPU gesendet wird.

Zusammengesetzt
Abbildung 18: Erstellung des Compositor-Threads für Zusammensetzungs-Frames Der Frame wird an den Browserprozess und dann an die GPU gesendet.

Der Vorteil des Compositing besteht darin, dass dabei der Hauptthread nicht einbezogen wird. Der Compositor-Thread muss nicht auf die Stilberechnung oder JavaScript-Ausführung warten. Aus diesem Grund gelten ausschließlich zusammengesetzte Animationen als am besten für eine reibungslose Leistung. Muss Layout oder Paint neu berechnet werden, muss der Hauptthread beteiligt sein.

Zusammenfassung

In diesem Beitrag ging es um die Rendering-Pipeline vom Parsen bis zum Aufbau. Jetzt wissen Sie mehr über die Leistungsoptimierung einer Website.

Im nächsten und letzten Post dieser Reihe werfen wir einen genaueren Blick auf den zusammengesetzten Thread und sehen uns an, was passiert, wenn Nutzereingaben wie mouse move und click eingehen.

Hat Ihnen der Beitrag gefallen? Wenn ihr Fragen oder Vorschläge für den zukünftigen Beitrag habt, könnt ihr euch gerne unten im Kommentarbereich oder unter @kosamari auf Twitter an uns wenden.

Weiter: Eingabe kommt an den Compositor