Ein Blick hinter die Kulissen eines modernen Webbrowsers (Teil 4)

Mariko Kosaka

Eingabe wird an den Compositor gesendet

Dies ist der letzte Teil der vierteiligen Blogreihe mit Einblicken in Chrome. Wir untersuchen, wie unser Code zum Anzeigen einer Website verarbeitet wird. Im vorherigen Beitrag haben wir uns den Rendering-Prozess und den Compositor angesehen. In diesem Beitrag sehen wir uns an, wie Compositor reibungslose Interaktionen bei Nutzereingaben ermöglicht.

Eingabeereignisse aus der Sicht des Browsers

Wenn Sie „Eingabeereignisse“ hören, denken Sie vielleicht nur an das Eintippen in ein Textfeld oder an einen Mausklick. Aus Sicht des Browsers bedeutet Eingabe jedoch jede Aktion des Nutzers. Das Scrollen mit dem Mausrad ist ein Eingabeereignis und auch das Berühren oder Bewegen der Maus ist ein Eingabeereignis.

Wenn ein Nutzer eine Touch-Geste auf dem Bildschirm ausführt, wird die Geste zuerst vom Browserprozess empfangen. Der Browserprozess weiß jedoch nur, wo diese Geste stattgefunden hat, da der Inhalt eines Tabs vom Rendererprozess verarbeitet wird. Der Browserprozess sendet also den Ereignistyp (z. B. touchstart) und seine Koordinaten an den Rendererprozess. Der Renderer-Prozess verarbeitet das Ereignis ordnungsgemäß, indem er das Ereignisziel findet und die zugehörigen Ereignis-Listener ausführt.

Eingabeereignis
Abbildung 1: Durch den Browserprozess an den Renderer-Prozess weitergeleitetes Eingabeereignis

Compositor empfängt Eingabeereignisse

Abbildung 2: Der Viewport schwebt über den Seitenebenen

Im vorherigen Beitrag haben wir uns angesehen, wie der Compositor durch das Zusammenführen von gerasterten Ebenen ein flüssiges Scrollen ermöglichen kann. Wenn der Seite keine Eingabeereignis-Listener angehängt sind, kann der Compositor-Thread einen neuen zusammengesetzten Frame erstellen, der völlig unabhängig vom Hauptthread ist. Was passiert aber, wenn der Seite Ereignis-Listener hinzugefügt wurden? Woher weiß der Compositor-Thread, ob das Ereignis verarbeitet werden muss?

Nicht schnell scrollbarer Bereich

Da das Ausführen von JavaScript die Aufgabe des Hauptthreads ist, kennzeichnet der Compositor-Thread beim Zusammensetzen einer Seite einen Bereich der Seite, an den Ereignishandler angehängt sind, als „Nicht schnell scrollbarer Bereich“. Anhand dieser Informationen kann der Compositor-Thread dafür sorgen, dass das Eingabeereignis an den Hauptthread gesendet wird, wenn das Ereignis in dieser Region auftritt. Wenn das Eingabeereignis außerhalb dieser Region stammt, fährt der Compositor-Thread mit dem Erstellen des neuen Frames fort, ohne auf den Hauptthread zu warten.

begrenzte Region, die nicht schnell scrollbar ist
Abbildung 3: Diagramm der beschriebenen Eingabe in den Bereich, der nicht schnell scrollbar ist

Beachten Sie Folgendes, wenn Sie Ereignis-Handler schreiben

Ein gängiges Muster für die Ereignisbehandlung in der Webentwicklung ist die Ereignisdelegierung. Da Ereignisse weitergeleitet werden, können Sie einen Ereignishandler an das oberste Element anhängen und Aufgaben basierend auf dem Ereignisziel delegieren. Möglicherweise haben Sie schon einmal Code wie den unten stehenden gesehen oder geschrieben.

document.body.addEventListener('touchstart', event => {
    if (event.target === area) {
        event.preventDefault();
    }
});

Da Sie nur einen Ereignishandler für alle Elemente schreiben müssen, ist die Ergonomie dieses Ereignisdelegierungsmusters attraktiv. Wenn Sie sich diesen Code jedoch aus der Perspektive des Browsers ansehen, ist jetzt die gesamte Seite als Bereich gekennzeichnet, der nicht schnell scrollbar ist. Das bedeutet, dass der Compositor-Thread auch dann mit dem Haupt-Thread kommunizieren und jedes Mal auf ihn warten muss, wenn ein Eingabeereignis eintritt, selbst wenn Ihre Anwendung keine Eingaben von bestimmten Teilen der Seite benötigt. Dadurch wird die Funktion zum flüssigen Scrollen des Renderers beeinträchtigt.

Vollständige Seite, kein schnelles Scrollen
Abbildung 4: Diagramm der beschriebenen Eingabe in den Bereich, der sich nicht schnell scrollen lässt und eine ganze Seite umfasst

Du kannst das verhindern, indem du passive: true-Optionen in deinem Ereignis-Listener übergibst. Dies weist den Browser darauf hin, dass Sie das Ereignis weiterhin im Hauptthread überwachen möchten, der Compositor aber auch einen neuen Frame zusammenstellen kann.

document.body.addEventListener('touchstart', event => {
    if (event.target === area) {
        event.preventDefault()
    }
 }, {passive: true});

Prüfen, ob das Ereignis abgebrochen werden kann

Seitenscrollen
Abbildung 5: Webseite, bei der ein Teil der Seite auf horizontales Scrollen fixiert ist

Angenommen, Sie haben ein Feld auf einer Seite, für das Sie die Scrollrichtung auf horizontales Scrollen beschränken möchten.

Wenn Sie die Option passive: true in Ihrem Zeigerereignis verwenden, kann das Seitenscrollen flüssig sein. Das vertikale Scrollen hat jedoch möglicherweise bereits begonnen, wenn Sie preventDefault verwenden möchten, um die Scrollrichtung einzuschränken. Mit der Methode event.cancelable können Sie das überprüfen.

document.body.addEventListener('pointermove', event => {
    if (event.cancelable) {
        event.preventDefault(); // block the native scroll
        /*
        *  do what you want the application to do here
        */
    }
}, {passive: true});

Alternativ können Sie eine CSS-Regel wie touch-action verwenden, um den Ereignis-Handler vollständig zu entfernen.

#area {
  touch-action: pan-x;
}

Ereignisziel ermitteln

Trefferprüfung
Abbildung 6: Der Haupt-Thread, der die Paint-Einträg betrachtet und fragt, was am Punkt x.y gezeichnet wird

Wenn der Compositor-Thread ein Eingabeereignis an den Hauptthread sendet, wird zuerst ein Treffertest ausgeführt, um das Ereignisziel zu finden. Beim Kollisionstest werden Daten aus Paint-Eintragsdaten verwendet, die beim Rendering generiert wurden, um herauszufinden, was sich unter den Punktkoordinaten befindet, an denen das Ereignis aufgetreten ist.

Minimieren von Ereignisweiterleitungen an den Haupt-Thread

Im vorherigen Beitrag haben wir erläutert, wie ein typischer Bildschirm den Bildschirm 60-mal pro Sekunde aktualisiert und wie wir den Rhythmus für eine flüssige Animation erfüllen müssen. Ein typisches Touchscreen-Gerät sendet 60 bis 120 Mal pro Sekunde Touch-Ereignisse und eine typische Maus sendet 100 Mal pro Sekunde Ereignisse. Das Eingabeereignis hat eine höhere Auflösung als unser Display aktualisieren kann.

Wenn ein kontinuierliches Ereignis wie touchmove 120 Mal pro Sekunde an den Hauptthread gesendet wird, kann dies im Vergleich zur Geschwindigkeit, mit der der Bildschirm aktualisiert werden kann, zu einer übermäßigen Anzahl von Treffertests und JavaScript-Ausführungen führen.

Ungefilterte Ereignisse
Abbildung 7: Zu viele Ereignisse überfluten die Zeitleiste des Frames und verursachen Ruckler auf der Seite

Chrome fasst kontinuierliche Ereignisse wie wheel, mousewheel, mousemove, pointermove, touchmove zusammen und verzögert die Weiterleitung bis zur nächsten requestAnimationFrame, um übermäßig viele Aufrufe an den Hauptthread zu minimieren.

Zusammengeführte Ereignisse
Abbildung 8: Gleiche Zeitachse wie zuvor, aber Ereignis wird zusammengeführt und verzögert

Alle diskreten Ereignisse wie keydown, keyup, mouseup, mousedown, touchstart und touchend werden sofort gesendet.

getCoalescedEvents verwenden, um Intraframe-Ereignisse abzurufen

Für die meisten Webanwendungen sollten zusammengeführte Ereignisse ausreichen, um eine gute Nutzererfahrung zu bieten. Wenn Sie jedoch Elemente wie eine Zeichenanwendung erstellen und einen Pfad auf der Grundlage von touchmove-Koordinaten festlegen, können die Koordinaten zwischen den Koordinaten verloren gehen, um eine gleichmäßige Linie zu zeichnen. In diesem Fall können Sie die Methode getCoalescedEvents im Zeigerereignis verwenden, um Informationen zu diesen zusammengeführten Ereignissen zu erhalten.

getCoalescedEvents
Abbildung 9: Glatter Touch-Gestepfad links, zusammengeführter begrenzter Pfad rechts
window.addEventListener('pointermove', event => {
    const events = event.getCoalescedEvents();
    for (let event of events) {
        const x = event.pageX;
        const y = event.pageY;
        // draw a line using x and y coordinates.
    }
});

Nächste Schritte

In dieser Reihe haben wir uns mit der Funktionsweise eines Webbrowsers befasst. Falls ihr noch nie darüber nachgedacht habt, warum in den Entwicklertools das Hinzufügen von {passive: true} zu eurem Event-Handler oder warum das async-Attribut in eurem Script-Tag sinnvoll sein könnte, hoffe ich, dass euch diese Reihe aufgezeigt hat, warum ein Browser diese Informationen benötigt, um eine schnellere und reibungslosere Webnutzung zu ermöglichen.

Lighthouse verwenden

Wenn Sie Ihren Code für den Browser ansprechend gestalten möchten, aber nicht wissen, wo Sie anfangen sollen, dann ist Lighthouse ein Tool, das alle Websites überprüft und Berichte darüber liefert, was richtig gemacht wird und was verbessert werden muss. Das Lesen der Liste der Audits gibt Ihnen auch eine Vorstellung davon, was für einen Browser wichtig ist.

Leistung messen

Leistungsoptimierungen können für verschiedene Websites variieren. Daher ist es wichtig, dass Sie die Leistung Ihrer Website messen und entscheiden, was am besten zu Ihrer Website passt. Das Chrome DevTools-Team hat einige Anleitungen zum Messen der Leistung Ihrer Website veröffentlicht.

Richtlinie zu Funktionen auf Ihrer Website hinzufügen

Wenn Sie noch einen Schritt weitergehen möchten, ist die Richtlinie zu Funktionen eine neue Funktion der Webplattform, die Ihnen beim Erstellen Ihres Projekts als Leitfaden dienen kann. Wenn Sie die Richtlinie für Funktionen aktivieren, wird das Verhalten Ihrer App festgelegt und Sie können Fehler vermeiden. Wenn Sie beispielsweise dafür sorgen möchten, dass Ihre App das Parsen nie blockiert, können Sie die Richtlinie für synchrone Scripts verwenden. Wenn sync-script: 'none' aktiviert ist, wird JavaScript-Code, der den Parser blockiert, nicht ausgeführt. So wird verhindert, dass Ihr Code den Parser blockiert, und der Browser muss den Parser nicht pausieren.

Zusammenfassung

Danke

Als ich anfing, Websites zu erstellen, ging es mir fast nur darum, wie ich meinen Code schreiben und wie ich produktiver werden könnte. Diese Dinge sind wichtig, aber wir sollten auch berücksichtigen, wie der Browser den von uns geschriebenen Code annimmt. Moderne Browser arbeiten kontinuierlich daran, die Nutzererfahrung im Web zu verbessern. Wenn wir unseren Code so strukturieren, dass er für den Browser möglichst nutzerfreundlich ist, verbessert sich auch die Nutzerfreundlichkeit. Ich hoffe, Sie schließen sich mir an, um die Browser zu unterstützen.

Vielen Dank an alle, die frühe Entwürfe dieser Reihe geprüft haben, darunter: Alex Russell, Paul Irish, Meggin Kearney, Eric Bidelman, Mathias Bynens, Addy Osmani, Kinuko Yasuda, Nasko Oskov und Charlie Reis.

Hat Ihnen diese Reihe 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.