Bessere JS-Planung mit isInputPending()

Eine neue JavaScript API, mit der Sie einen Kompromiss zwischen Ladeleistung und Reaktionsschnelligkeit bei Eingaben vermeiden können.

Nate Schloss
Nate Schloss
Andrew Comminos
Andrew Comminos

Ein schnelles Laden ist schwierig. Websites, die JavaScript zum Rendern ihrer Inhalte nutzen, müssen derzeit einen Kompromiss zwischen Ladeleistung und Eingabereaktionsfähigkeit eingehen: entweder alle für die Anzeige erforderlichen Vorgänge auf einmal ausführen (bessere Ladeleistung, schlechtere Eingabereaktionsfähigkeit) oder die Arbeit in kleinere Aufgaben aufteilen, um auf Eingabe und Paint (schlechtere Ladeleistung, bessere Eingabereaktionsfähigkeit) zu bleiben.

Um diese Kompromisse zu vermeiden, hat Facebook die isInputPending() API in Chromium vorgeschlagen und implementiert, um die Reaktionsfähigkeit ohne Resultate zu verbessern. Aufgrund des Feedbacks von Ursprungstests haben wir einige Aktualisierungen an der API vorgenommen und freuen uns, Ihnen mitteilen zu können, dass die API jetzt standardmäßig in Chromium 87 verfügbar ist.

Browserkompatibilität

Unterstützte Browser

  • 87
  • 87
  • x
  • x

isInputPending() wird ab Version 87 in Chromium-basierten Browsern ausgeliefert. Kein anderer Browser hat eine Absicht zum Versenden der API signalisiert.

Hintergrund

Im heutigen JS-System wird die meiste Arbeit in einem einzigen Thread ausgeführt: dem Hauptthread. Dies bietet Entwicklern ein robustes Ausführungsmodell. Die Nutzerfreundlichkeit (insbesondere die Reaktionszeit) kann jedoch stark beeinträchtigt werden, wenn das Skript über einen längeren Zeitraum ausgeführt wird. Verarbeitet die Seite während des Auslösens eines Eingabeereignisses viel Arbeit, verarbeitet sie das Klickeingabeereignis erst, wenn diese Aufgabe abgeschlossen ist.

Die aktuelle Best Practice besteht darin, dieses Problem durch Aufteilen des JavaScript in kleinere Blöcke zu lösen. Während des Ladens der Seite kann JavaScript ausgeführt werden, die Steuerung wird dann an den Browser zurückgegeben und an den Browser zurückgegeben. Der Browser kann dann seine Eingabeereigniswarteschlange prüfen und feststellen, ob es etwas gibt, über das die Seite informiert werden muss. Die JavaScript-Blöcke werden nach dem Hinzufügen wieder ausgeführt. Das hilft, kann aber andere Probleme verursachen.

Jedes Mal, wenn die Seite die Kontrolle an den Browser zurückgibt, dauert es einige Zeit, bis der Browser die Eingabeereigniswarteschlange überprüft, Ereignisse verarbeitet und den nächsten JavaScript-Block erfasst hat. Während der Browser schneller auf Ereignisse reagiert, verlangsamt sich die Ladezeit der Seite insgesamt. Und wenn wir zu oft etwas ausgeben, wird die Seite zu langsam geladen. Wenn weniger oft Ergebnisse geliefert werden, dauert es länger, bis der Browser auf Nutzerereignisse reagiert, und die Nutzer sind frustriert. Kein Spaß.

Ein Diagramm, das zeigt, dass der Browser bei der Ausführung langer JS-Aufgaben weniger Zeit hat, um Ereignisse auszulösen.

Bei Facebook wollten wir herausfinden, wie alles aussehen würde, wenn wir einen neuen Ladeansatz entwickeln würden, der diesen frustrierenden Kompromiss beseitigen würde. Wir haben unsere Freunde bei Chrome darüber informiert und einen Vorschlag für isInputPending() gemacht. Die isInputPending() API ist die erste, die das Konzept von Unterbrechungen für Nutzereingaben im Web verwendet. Sie ermöglicht, dass JavaScript Eingaben überprüfen kann, ohne dem Browser nachzugeben.

Ein Diagramm, das zeigt, dass mit der Funktion "isInputPending()" überprüft werden kann, ob eine ausstehende Nutzereingabe vorliegt, ohne die Ausführung an den Browser vollständig auszuführen.

Da Interesse an der API besteht, haben wir uns mit unseren Kollegen bei Chrome zusammengetan, um die Funktion in Chromium zu implementieren und auszuliefern. Mit der Unterstützung der Chrome-Entwickler haben wir die Patches in einen Ursprungstest gebracht. So kann Chrome Änderungen testen und Feedback von Entwicklern einholen, bevor wir eine API vollständig veröffentlichen.

Wir haben nun Feedback aus dem Ursprungstest und den anderen Mitgliedern der W3C Web Performance Working Group aufgenommen und Änderungen an der API vorgenommen.

Beispiel: ein erfolgreicherer Planer

Nehmen wir an, Sie müssen einiges zu tun haben, um Ihre Seite zu laden, zum Beispiel müssen Sie Markup aus Komponenten generieren, Primzahlen ausklammern oder einfach ein Ladesymbol zeichnen. Jede dieser Aufgaben wird in ein eigenständiges Arbeitselement unterteilt. Skizzieren wir mithilfe des Planermusters, wie wir unsere Arbeit in einer hypothetischen processWorkQueue()-Funktion verarbeiten könnten:

const DEADLINE = performance.now() + QUANTUM;
while (workQueue.length > 0) {
  if (performance.now() >= DEADLINE) {
    // Yield the event loop if we're out of time.
    setTimeout(processWorkQueue);
    return;
  }
  let job = workQueue.shift();
  job.execute();
}

Wenn Sie processWorkQueue() später in einer neuen Makroaufgabe über setTimeout() aufrufen, kann der Browser relativ ununterbrochen auf Eingaben reagieren (er kann Event-Handler ausführen, bevor die Arbeit fortgesetzt wird). Es kann jedoch vorkommen, dass andere Arbeiten, die Kontrolle über die Ereignisschleife haben, für längere Zeit außer Betrieb sind oder eine Ereignislatenz von bis zu QUANTUM Millisekunden zusätzlich erhöhen möchten.

Das ist okay, aber können wir es besser machen? Ja, klar!

const DEADLINE = performance.now() + QUANTUM;
while (workQueue.length > 0) {
  if (navigator.scheduling.isInputPending() || performance.now() >= DEADLINE) {
    // Yield if we have to handle an input event, or we're out of time.
    setTimeout(processWorkQueue);
    return;
  }
  let job = workQueue.shift();
  job.execute();
}

Durch den Aufruf von navigator.scheduling.isInputPending() können wir schneller auf Eingaben reagieren und gleichzeitig dafür sorgen, dass unsere Aktivitäten zum Blockieren von Anzeigen ohne Unterbrechung ausgeführt werden. Wenn wir bis zum Abschluss der Arbeit nichts anderes als Input (z.B. Painting) verarbeiten möchten, können wir auch die Länge von QUANTUM etwas erhöhen.

Standardmäßig werden „Dauerhafte“ Ereignisse von isInputPending() nicht zurückgegeben. Dazu gehören mousemove, pointermove und weitere. Wenn Sie auch hier Ergebnisse erreichen möchten, ist das kein Problem. Wenn Sie für isInputPending() ein Objekt angeben und includeContinuous auf true gesetzt ist, kann losgehen:

const DEADLINE = performance.now() + QUANTUM;
const options = { includeContinuous: true };
while (workQueue.length > 0) {
  if (navigator.scheduling.isInputPending(options) || performance.now() >= DEADLINE) {
    // Yield if we have to handle an input event (any of them!), or we're out of time.
    setTimeout(processWorkQueue);
    return;
  }
  let job = workQueue.shift();
  job.execute();
}

Fertig! Frameworks wie React integrieren die isInputPending()-Unterstützung mithilfe einer ähnlichen Logik in ihre zentralen Planungsbibliotheken. Dies führt hoffentlich dazu, dass Entwickler, die diese Frameworks verwenden, im Hintergrund von isInputPending() ohne große Umschreibungen profitieren können.

Sich etwas zu verdienen, ist nicht immer schlecht

Dabei ist zu beachten, dass eine geringere Menge nicht die richtige Lösung für jeden Anwendungsfall ist. Es gibt viele Gründe, die Steuerung an den Browser zurückzugeben, statt Eingabeereignisse zu verarbeiten, z. B. um Rendering und andere Skripts auf der Seite auszuführen.

Es gibt Fälle, in denen der Browser ausstehende Eingabeereignisse nicht richtig zuordnen kann. Insbesondere, wenn komplexe Clips und Masken für ursprungsübergreifende iFrames festgelegt werden, können falsch negative Ergebnisse gemeldet werden (d.h. isInputPending() kann beim Targeting dieser Frames unerwartet „false“ zurückgeben). Achten Sie darauf, oft genug Ergebnisse zu erzielen, wenn Interaktionen mit stilisierten Subframes erforderlich sind.

Achten Sie auch auf andere Seiten, die eine Ereignisschleife haben. Auf Plattformen wie Chrome für Android ist es üblich, dass sich mehrere Ursprünge eine Ereignisschleife teilen. isInputPending() gibt niemals true zurück, wenn die Eingabe an einen ursprungsübergreifenden Frame weitergeleitet wird. Daher können Seiten im Hintergrund die Reaktionsfähigkeit von Vordergrundseiten beeinträchtigen. Wenn Sie im Hintergrund arbeiten, können Sie die Anzahl Ihrer Anzeigen reduzieren, die Website auf einen späteren Zeitpunkt verschieben oder mehr Einnahmen erzielen, wenn Sie die Page visibility API im Hintergrund ausführen.

Wir empfehlen Ihnen, isInputPending() mit Bedacht zu verwenden. Wenn keine Nutzer blockiert werden müssen, solltest du den Nutzern in der Ereignisschleife häufiger nachkommen. Lange Aufgaben können schädlich sein.

Feedback

  • Geben Sie Feedback zur Spezifikation im Repository is-input-pending ein.
  • Wenden Sie sich auf Twitter an @acomminos (einer der Spezifikationsautoren).

Fazit

Wir freuen uns, dass isInputPending() eingeführt wird und Entwickler es ab heute nutzen können. Diese API ist das erste Mal, dass Facebook eine neue Web-API erstellt hat, die von der Ideenentwicklung zu einem Standardvorschlag für den tatsächlichen Versand in einem Browser genutzt wurde. Wir möchten uns bei allen bedanken, die uns geholfen haben, diesen Punkt zu erreichen, und wir bedanken uns bei allen Chrome-Mitarbeitern, die uns bei der Ausarbeitung dieser Idee und der Umsetzung geholfen haben.

Hero-Foto von Will H McMahan auf Unsplash