Bessere JS-Planung mit isInputPending()

Eine neue JavaScript API, mit der Sie einen Kompromiss zwischen Ladeleistung und Reaktionsfähigkeit bei der Eingabe vermeiden können.

Nate Schloss
Nate Schloss
Andrew Comminos
Andrew Comminos

Ein schnelles Laden ist schwierig. Bei Websites, die JS zum Rendern ihrer Inhalte verwenden, müssen derzeit Kompromisse zwischen Ladeleistung und Eingabereaktionsfähigkeit eingegangen werden: Entweder wird die gesamte für die Anzeige erforderliche Arbeit auf einmal ausgeführt (bessere Ladeleistung, schlechtere Eingabereaktionsfähigkeit) oder die Arbeit wird in kleinere Aufgaben unterteilt, um auf Eingaben und das Ausführen von Aktionen zu reagieren (schlechtere Ladeleistung, bessere Eingabereaktionsfähigkeit).

Um diese Abwägung zu vermeiden, hat Facebook die isInputPending() API in Chromium vorgeschlagen und implementiert, um die Reaktionsfähigkeit zu verbessern, ohne Abstriche zu machen. Auf Grundlage des Feedbacks aus dem Ursprungstest haben wir die API aktualisiert. Sie ist jetzt standardmäßig in Chromium 87 verfügbar.

Browserkompatibilität

Unterstützte Browser

  • Chrome: 87.
  • Edge: 87.
  • Firefox: Nicht unterstützt.
  • Safari: Nicht unterstützt.

Quelle

isInputPending() ist in Chromium-basierten Browsern ab Version 87 verfügbar. Kein anderer Browser hat die Absicht signalisiert, die API zu versenden.

Hintergrund

Die meiste Arbeit im heutigen JS-System wird in einem einzigen Thread erledigt: dem Hauptthread. Dies bietet Entwicklern ein robustes Ausführungsmodell, aber die Nutzerfreundlichkeit (insbesondere die Reaktionsfähigkeit) kann drastisch beeinträchtigt werden, wenn das Script lange ausgeführt wird. Wenn die Seite viel Arbeit macht, während ein Eingabeereignis ausgelöst wird, verarbeitet sie das Klickeingabeereignis erst, nachdem die Arbeit abgeschlossen ist.

Die aktuelle Best Practice besteht darin, das JavaScript in kleinere Blöcke aufzuteilen. Während die Seite geladen wird, kann ein wenig JavaScript ausgeführt werden, bevor die Kontrolle an den Browser zurückgegeben wird. Der Browser kann dann seine Eingabeereigniswarteschlange prüfen und sehen, ob er der Seite etwas mitteilen muss. Sobald die JavaScript-Blöcke hinzugefügt werden, führt der Browser sie wieder aus. Das hilft zwar, kann aber zu anderen Problemen führen.

Jedes Mal, wenn die Seite die Steuerung an den Browser zurückgibt, dauert es einige Zeit, bis der Browser seine Eingabeereigniswarteschlange überprüft, Ereignisse verarbeitet und den nächsten JavaScript-Block aufnimmt. Während der Browser schneller auf Ereignisse reagiert, verlangsamt sich die Gesamtladezeit der Seite. Wenn wir zu oft nachgeben, lädt die Seite zu langsam. Wenn wir seltener nachgeben, dauert es länger, bis der Browser auf Nutzerereignisse reagiert, und die Nutzer werden frustriert. Das ist nicht lustig.

Ein Diagramm, das zeigt, dass der Browser bei langen JS-Aufgaben weniger Zeit zum Senden von Ereignissen hat.

Bei Facebook wollten wir herausfinden, wie es wäre, wenn wir einen neuen Ansatz für das Laden entwickeln würden, der diesen frustrierenden Kompromiss beseitigt. Wir haben uns an unsere Freunde bei Chrome gewandt und den Vorschlag für isInputPending() entwickelt. Die isInputPending() API ist die erste, die das Konzept von Unterbrechungen für Nutzereingaben im Web verwendet. So kann JavaScript nach Eingaben suchen, ohne an den Browser weiterzuleiten.

Ein Diagramm, das zeigt, dass mit isInputPending() in Ihrem JS-Code geprüft werden kann, ob eine Nutzereingabe ausstehend ist, ohne dass die Ausführung vollständig an den Browser zurückgegeben wird.

Da es Interesse an der API gab, haben wir uns mit unseren Kollegen von Chrome zusammengetan, um die Funktion in Chromium zu implementieren und zu veröffentlichen. Mit der Hilfe der Chrome-Entwickler haben wir die Patches in einem Ursprungstest entwickelt. Mit diesem Test kann Chrome Änderungen testen und Feedback von Entwicklern einholen, bevor wir eine API vollständig veröffentlichen.

Wir haben nun das Feedback aus dem Ursprungstest und von den anderen Mitgliedern der W3C Web Performance Working Group berücksichtigt und Änderungen an der API vorgenommen.

Beispiel: ein Yield-Optimierungstool

Angenommen, Sie müssen eine Reihe von Aufgaben blockieren, die die Anzeige blockieren, z. B. das Generieren von Markup aus Komponenten, das Herausrechnen von Primzahlen oder das Zeichnen eines coolen Ladesymbols. Jede dieser Aufgaben wird in ein einzelnes Arbeitselement unterteilt. Anhand des Scheduler-Musters skizzieren wir, 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 wir processWorkQueue() später in einer neuen Makroaufgabe über setTimeout() aufrufen, können wir den Browser so konfigurieren, dass er etwas reaktionsschneller auf Eingaben reagiert (Ereignishandler können ausgeführt werden, bevor die Arbeit fortgesetzt wird), und gleichzeitig relativ störungsfrei ausgeführt wird. Es kann jedoch sein, dass wir durch andere Aufgaben, die die Kontrolle über den Ereignis-Loop übernehmen, für lange Zeit aus der Planung verdrängt werden oder es zu einer zusätzlichen Ereignislatenz von bis zu QUANTUM Millisekunden kommt.

Das ist in Ordnung, aber können wir das besser machen? Auf jeden Fall!

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 einen Aufruf von navigator.scheduling.isInputPending() können wir schneller auf Eingaben reagieren und gleichzeitig dafür sorgen, dass unsere Displayblockierung ansonsten ungehindert ausgeführt wird. Wenn wir bis zum Abschluss der Arbeit nichts anderes als Eingaben (z.B. Malen) verarbeiten möchten, können wir auch die Länge von QUANTUM erhöhen.

Standardmäßig werden „fortlaufende“ Ereignisse von isInputPending() nicht zurückgegeben. Dazu gehören mousemove, pointermove und weitere. Wenn Sie auch für diese Daten die Einwilligung widerrufen möchten, ist das kein Problem. Wenn wir isInputPending() ein Objekt mit includeContinuous = true übergeben, ist alles in Ordnung:

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 bauen isInputPending()-Unterstützung mithilfe einer ähnlichen Logik in ihre grundlegenden Planungsbibliotheken ein. Wir hoffen, dass Entwickler, die diese Frameworks verwenden, von isInputPending() profitieren können, ohne dass sie ihre Codebasis erheblich umschreiben müssen.

Nachgeben ist nicht immer schlecht

Zu beachten ist, dass weniger Erträge nicht für jeden Anwendungsfall die richtige Lösung sind. Es gibt viele Gründe, die Kontrolle an den Browser zurückzugeben, z. B. zum Rendern und Ausführen anderer Scripts auf der Seite.

Es kann vorkommen, dass der Browser ausstehende Eingabeereignisse nicht richtig zuordnen kann. Insbesondere wenn Sie komplexe Clips und Masken für plattformübergreifende Iframes festlegen, werden möglicherweise falsch negative Ergebnisse zurückgegeben. Das bedeutet, dass isInputPending() beim Targeting auf diese Frames unerwartet „falsch“ zurückgibt. Achten Sie darauf, dass Sie die Funktion „Yield“ häufig genug verwenden, wenn Ihre Website Interaktionen mit stilisierten Subframes erfordert.

Achten Sie auch auf andere Seiten mit einer gemeinsamen Ereignisschleife. Auf Plattformen wie Chrome für Android ist es durchaus üblich, dass 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 Reaktion von Seiten im Vordergrund beeinträchtigen. Wenn Sie im Hintergrund arbeiten, können Sie mit der Page Visibility API die Auslieferung von Anzeigen reduzieren, verschieben oder häufiger pausieren.

Wir empfehlen, isInputPending() mit Bedacht zu verwenden. Wenn keine Nutzerblockierungen vorgenommen werden müssen, seien Sie freundlich zu anderen in der Ereignisschleife, indem Sie häufiger ein Ergebnis liefern. Lange Aufgaben können schädlich sein.

Feedback

  • Geben Sie Feedback zur Spezifikation im Repository is-input-pending.
  • Wenden Sie sich auf Twitter an @acomminos, einen der Autoren der Spezifikation.

Fazit

Wir freuen uns, dass isInputPending() eingeführt wird und Entwickler es ab heute verwenden können. Mit dieser API hat Facebook zum ersten Mal eine neue Web-API entwickelt und von der Idee über den Standardsvorschlag bis zur tatsächlichen Bereitstellung in einem Browser geführt. Wir möchten uns bei allen bedanken, die uns geholfen haben, zu diesem Punkt zu gelangen, und allen Chrome-Mitarbeitern danken, die uns geholfen haben, diese Idee zu verwirklichen und umzusetzen.

Hero-Foto von Will H McMahan auf Unsplash.