Innere Funktionsweise eines Renderer-Prozesses
Dies ist Teil 3 von 4 Teile der Blog-Reihe zur Funktionsweise von Browsern. Zuvor haben wir uns mit der Multi-Prozess-Architektur und dem Navigationsablauf beschäftigt. In diesem Beitrag sehen wir uns an, was im Renderer-Prozess passiert.
Der Renderer-Prozess wirkt sich auf viele Aspekte der Webleistung aus. Da im Renderer-Prozess viel los ist, bietet dieser Post nur einen allgemeinen Überblick. Wenn du mehr erfahren möchtest, findest du im Abschnitt „Leistung“ in den Web Fundamentals viele weitere Ressourcen.
Renderer-Prozesse verarbeiten Webinhalte
Der Renderer-Prozess ist für alle Vorgänge innerhalb eines Tabs verantwortlich. In einem Renderer-Prozess verarbeitet der Hauptthread den Großteil des Codes, den Sie an den Nutzer senden. Manchmal werden Teile Ihres JavaScript von Worker-Threads verarbeitet, wenn Sie einen Web Worker oder einen Service Worker verwenden. Compositor- und Raster-Threads werden ebenfalls innerhalb von Renderer-Prozessen ausgeführt, um eine Seite effizient und reibungslos zu rendern.
Die Hauptaufgabe des Rendererprozesses besteht darin, HTML-, CSS- und JavaScript-Code in eine Webseite umzuwandeln, mit der der Nutzer interagieren kann.
Parsen
DOM-Erstellung
Wenn der Rendererprozess eine Commit-Nachricht für eine Navigation empfängt und HTML-Daten empfängt, beginnt der Hauptthread mit dem Parsen des Textstrings (HTML) und der Umwandlung in ein Dokument-Object Model (DOM).
Das DOM ist die interne Darstellung der Seite in einem Browser sowie die Datenstruktur und die API, mit denen Webentwickler über JavaScript interagieren können.
Das Parsen eines HTML-Dokuments in ein DOM wird durch den HTML-Standard definiert. Sie haben vielleicht bemerkt, dass beim Einspeisen von HTML-Code in einen Browser nie ein Fehler ausgegeben wird. Wenn beispielsweise das schließende </p>
-Tag fehlt, ist es ein gültiger HTML-Code. Fehlerhaftes Markup wie Hi! <b>I'm <i>Chrome</b>!</i>
(b-Tag wird vor i-Tag geschlossen) wird 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 ordnungsgemäß behandelt werden. Wenn Sie wissen möchten, wie diese Vorgänge funktionieren, lesen Sie den Abschnitt Eine Einführung in Fehlerbehandlung und ungewöhnliche Fälle im Parser der HTML-Spezifikation.
Laden von Unterressourcen
Websites verwenden in der Regel externe Ressourcen wie Bilder, CSS und JavaScript. Diese Dateien müssen aus dem Netzwerk oder dem Cache geladen werden. Der Hauptthread könnte sie beim Parsen nacheinander anfordern, um ein DOM zu erstellen. Zur Beschleunigung wird jedoch gleichzeitig „Preload Scanner“ ausgeführt.
Wenn das HTML-Dokument Dinge wie <img>
oder <link>
enthält, prüft der Scanner beim Vorabladen Tokens, die vom HTML-Parser generiert wurden, und sendet Anfragen an den Netzwerkthread im Browserprozess.
JavaScript kann das Parsing blockieren
Wenn der HTML-Parser ein <script>
-Tag findet, pausiert er das Parsen des HTML-Dokuments und muss den JavaScript-Code laden, parsen und ausführen. Der Grund dafür ist, dass JavaScript die Form des Dokuments ändern kann, z. B. mit document.write()
, wodurch die gesamte DOM-Struktur geändert wird. In der Übersicht über das Parsing-Modell in der HTML-Spezifikation finden Sie ein schönes Diagramm. Aus diesem Grund muss der HTML-Parser warten, bis JavaScript ausgeführt wurde, bevor er mit dem Parsen des HTML-Dokuments fortfahren kann. Wenn du neugierig bist, was bei der JavaScript-Ausführung passiert, findest du hier entsprechende Vorträge und Blogposts des V8-Teams.
Hinweis an den Browser, wie Ressourcen geladen werden sollen
Es gibt viele Möglichkeiten, wie Webentwickler dem Browser Hinweise senden können, um Ressourcen zu laden.
Wenn document.write()
in Ihrem JavaScript nicht verwendet wird, können Sie dem <script>
-Tag das Attribut async
oder defer
hinzufügen. Der Browser lädt dann den JavaScript-Code asynchron und führt ihn aus. Das Parsing wird nicht blockiert. Bei Bedarf können Sie auch das JavaScript-Modul verwenden. Mit <link rel="preload">
teilen Sie dem Browser mit, dass die Ressource für die aktuelle Navigation unbedingt erforderlich ist und Sie sie so schnell wie möglich herunterladen möchten. Weitere Informationen dazu finden Sie unter Ressourcenpriorisierung – Der Browser ist hilfreich.
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 jedes Element angewendet wird. Diese Informationen finden Sie im Abschnitt computed
der Entwicklertools.
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 für jedes Element werden Ränder definiert. Das liegt daran, dass der Browser
ein Standard-Stylesheet verwendet. Wenn Sie wissen möchten, wie der Standard-CSS-Code von Chrome aussieht, können Sie sich den Quellcode hier ansehen.
Layout
Jetzt kennt der Renderer-Prozess 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 möchten einem Freund ein Gemälde per Telefon beschreiben. „Es gibt einen großen roten Kreis und ein kleines blaues Quadrat“ reicht nicht aus, um zu wissen, wie das Gemälde genau aussehen soll.
Das Layout ist ein Prozess, um die Geometrie der Elemente zu finden. Der Hauptthread durchläuft das DOM und berechnete Stile und erstellt die Layoutstruktur, die Informationen wie x-y-Koordinaten und Begrenzungsrahmengrößen enthält. Der Layoutbaum hat eine ähnliche Struktur wie der DOM-Baum, enthält aber nur Informationen zu den auf der Seite sichtbaren Elementen. Wird display: none
angewendet, ist dieses Element nicht Teil der Layoutstruktur. Ein Element mit visibility: hidden
befindet sich jedoch darin. Wenn eine Pseudoklasse mit Inhalten wie p::before{content:"Hi!"}
angewendet wird, ist sie in der Layoutstruktur enthalten, obwohl sie nicht im DOM enthalten ist.
Das Festlegen des Layouts einer Seite ist eine anspruchsvolle Aufgabe. Auch beim einfachsten Seitenlayout wie einem Blockfluss von oben nach unten müssen Sie berücksichtigen, wie groß die Schrift ist und an welcher Stelle der Zeilenumbruch erfolgen soll, da diese sich auf die Größe und Form eines Absatzes auswirken und sich dann darauf auswirken, wo der folgende Absatz platziert werden muss.
Mit CSS kann das Element auf einer Seite schweben, Überlaufelemente maskiert und die Schreibrichtung geändert werden. Sie können sich vorstellen, dass diese Layoutphase eine große Aufgabe hat. In Chrome arbeitet ein ganzes Team von Ingenieuren an dem Layout. Wenn Sie sich Details zu ihrer Arbeit ansehen möchten, werden einige Vorträge der BlinkOn Conference aufgezeichnet und sind ziemlich interessant.
Farben
DOM, Stil und 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 von Elementen, müssen aber trotzdem beurteilen, in welcher Reihenfolge Sie sie zeichnen.
Beispielsweise kann für bestimmte Elemente z-index
festgelegt werden. In diesem Fall führt das Malen in der Reihenfolge der in den HTML-Code geschriebenen Elemente zu einem falschen Rendering.
In diesem Zeichenschritt läuft der Hauptthread durch den Layoutbaum, um Paint-Datensätze zu erstellen. Paint Record ist ein Hinweis zum Painting-Prozess, z. B. „Hintergrund zuerst, dann Text und dann Rechteck“. Wenn Sie <canvas>
-Elemente mit JavaScript gezeichnet haben, ist Ihnen dieser Vorgang möglicherweise bekannt.
Das Aktualisieren der Rendering-Pipeline ist kostspielig
Das Wichtigste in 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. Animationen erscheinen für das menschliche Auge flüssig, wenn Sie in jedem Frame etwas über den Bildschirm bewegen. Wenn bei der Animation jedoch die dazwischen liegenden Frames fehlen, erscheint die Seite „auffällig“.
Auch wenn Ihre Rendering-Vorgänge mit der Bildschirmaktualisierung Schritt halten, werden diese Berechnungen im Hauptthread ausgeführt. Dies bedeutet, dass dieser möglicherweise blockiert wird, wenn Ihre Anwendung JavaScript ausführt.
Der JavaScript-Vorgang lässt sich in kleine Blöcke unterteilen und mit requestAnimationFrame()
so planen, dass sie bei jedem Frame ausgeführt werden. 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 zu verhindern, dass der Hauptthread blockiert wird.
Aufbau
Wie würden Sie eine Seite zeichnen?
Der Browser kennt nun die Struktur des Dokuments, den Stil der einzelnen Elemente, die Geometrie der Seite und die Farbreihenfolge. Wie wird eine Seite gezeichnet? Die Umwandlung dieser Informationen in Pixel auf dem Bildschirm wird Rasterung genannt.
Eine einfache Möglichkeit wäre das Rastern von Teilen innerhalb des Darstellungsbereichs. Wenn ein Nutzer auf der Seite scrollt, dann den Rasterframe verschieben und die fehlenden Teile durch weitere Raster ergänzen kann. So hat Chrome das Rastern bei der Erstveröffentlichung behandelt. In modernen Browsern wird jedoch ein komplexerer Prozess ausgeführt, der als "compositing" bezeichnet wird.
Was ist Compositing?
Das Compositing ist eine Technik, um Teile einer Seite in Ebenen zu unterteilen, sie separat zu rastern und als Seite in einem separaten Thread, den sogenannten Compositor-Thread, zusammenzusetzen. Da die Ebenen bereits gerastert sind, muss beim Scrollen nur ein neuer Frame zusammengesetzt werden. Animationen können auf die gleiche Weise erreicht werden, indem Ebenen verschoben und ein neuer Frame zusammengesetzt wird.
Wie Ihre Website in Ebenen unterteilt ist, können Sie in den Entwicklertools im Bereich „Ebenen“ sehen.
In Ebenen unterteilen
Um herauszufinden, welche Elemente in welchen Ebenen enthalten sein müssen, läuft der Hauptthread durch den Layoutbaum, um den Ebenenbaum zu erstellen. Dieser Teil wird im Leistungssteuerfeld der Entwicklertools „Update Layer Tree“ genannt. Wenn bestimmte Bereiche einer Seite, die ein separater Layer sein sollen (z. B. das seitliche eingeschobene Menü), keine Ebene erhalten, können Sie dem Browser mithilfe des will-change
-Attributs in CSS einen Hinweis senden.
Sie könnten versucht sein, jedem Element Ebenen zuzuweisen, aber das Zusammensetzen über eine übermäßige Anzahl von Ebenen könnte den Vorgang verlangsamen als das Rastern kleiner Teile einer Seite für jeden Frame. Daher ist es wichtig, dass Sie die Rendering-Leistung Ihrer Anwendung messen. Weitere Informationen finden Sie unter Eigenschaften nur für zusammengesetzte Objekte beibehalten und Anzahl der Ebenen verwalten.
Raster und Zusammensetzung des Hauptthreads
Nachdem die Ebenenstruktur erstellt und die Paint-Reihenfolgen festgelegt sind, überträgt der Hauptthread diese Informationen an den Compositor-Thread. Der Compositor-Thread rastert dann jede Ebene. Eine Ebene kann wie die gesamte Länge einer Seite groß sein. Der Compositor-Thread teilt sie daher in Kacheln auf und sendet jede Kachel an Rasterthreads. Rasterthreads rastern jede Kachel und speichern sie im GPU-Arbeitsspeicher.
Der Compositor-Thread kann verschiedene Rasterthreads priorisieren, sodass Dinge innerhalb des Darstellungsbereichs (oder in der Nähe) zuerst gerastert werden können. Eine Ebene hat außerdem mehrere Kacheln für verschiedene Auflösungen, um z. B. Aktionen wie das Heranzoomen zu verarbeiten.
Nach dem Rastern der Kacheln erfasst der Compositor-Thread Kachelinformationen, die sogenannten Draws, um einen Compositor-Frame zu erstellen.
Quadrate zeichnen | Enthält Informationen wie die Position der Kachel im Speicher und die Stelle auf der Seite, an der die Kachel unter Berücksichtigung der Seitenzusammensetzung gezeichnet werden soll. |
Compositor-Frame | Eine Sammlung von Zeichenquadraten, die einen Frame einer Seite darstellen. |
Ein Compositor-Frame wird dann über IPC an den Browserprozess gesendet. An dieser Stelle könnte ein weiterer Compositor-Frame aus dem UI-Thread für die Änderung der Browser-UI oder aus anderen Rendererprozessen für Erweiterungen hinzugefügt werden. Diese Compositor-Frames werden an die GPU gesendet, um sie auf einem Bildschirm anzuzeigen. Wenn ein Scroll-Ereignis eingeht, erstellt der Compositor-Thread einen weiteren Compositor-Frame, der an die GPU gesendet wird.
Der Vorteil der Zusammensetzung besteht darin, dass der Hauptthread nicht einbezogen wird. Der Compositor-Thread muss nicht auf die Stilberechnung oder JavaScript-Ausführung warten. Aus diesem Grund gelten Animationen, die sich nur zusammensetzen, als am besten für eine reibungslose Leistung. Wenn Layout oder Paint neu berechnet werden muss, muss der Hauptthread einbezogen werden.
Zusammenfassung
In diesem Beitrag haben wir die Rendering-Pipeline vom Parsing bis zur Erstellung behandelt. Hoffentlich haben Sie jetzt die nötigen Kenntnisse, um mehr über die Leistungsoptimierung einer Website zu erfahren.
Im nächsten und letzten Beitrag dieser Reihe sehen wir uns den Compositor-Thread genauer an 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 gerne unten im Kommentarbereich oder unter @kosamari auf Twitter Beiträge von euch hören.