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.
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.
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.
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.
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.
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
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.
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.
Das Aktualisieren der Rendering-Pipeline ist kostspielig
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.
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.
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.
Compositing
Wie würden Sie eine Seite zeichnen?
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?
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.
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.
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.
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.