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 meisten Aufgaben im heutigen JS-System werden in einem einzigen Thread ausgeführt: 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, den JavaScript-Code in kleinere Blöcke zu unterteilen, um dieses Problem zu beheben. 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 feststellen, ob er der Seite etwas mitteilen muss. Der Browser kann dann wieder mit dem Ausführen der JavaScript-Blöcke fortfahren, sobald sie hinzugefügt wurden. 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. Der Browser reagiert zwar schneller auf Ereignisse, die Gesamtladezeit der Seite wird jedoch verlangsamt. 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 „isInputPending()“ Ihrem JS-Code ist, zu prüfen, ob eine Nutzereingabe ausstehend ist, ohne dass die Ausführung vollständig an den Browser erfolgt.

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. Mithilfe der Chrome-Entwickler konnten wir die Patches in einem Ursprungstest veröffentlichen. So können Änderungen in Chrome getestet und Feedback von Entwicklern eingeholt werden, bevor eine API vollständig veröffentlicht wird.

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

Beispiel: Yieldier-Planer

Angenommen, Sie müssen beim Laden Ihrer Seite eine Menge Arbeit leisten, die das Display blockiert, z. B. Markup aus Komponenten generieren, Primzahlen herausrechnen oder einfach nur einen coolen Ladebalken zeichnen. Jedes davon wird in ein separates 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 Eingabe (z.B. Painting) verarbeiten möchten, können wir auch die Länge von QUANTUM problemlos erhöhen.

Standardmäßig werden keine „kontinuierlichen“ Ereignisse von isInputPending() 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 mit ähnlicher Logik in ihre Kernbibliotheken für die Planung 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

Es ist jedoch wichtig zu beachten, dass eine geringere Ausbeute nicht für jeden Anwendungsfall die richtige Lösung ist. Es gibt viele Gründe, die Steuerung an den Browser zurückzugeben, außer zur Verarbeitung von Eingabeereignissen, wie z. B. für das Rendern und das Ausführen anderer Skripts auf der Seite.

Es kann vorkommen, dass der Browser ausstehende Eingabeereignisse nicht richtig zuordnen kann. Insbesondere beim Festlegen komplexer Clips und Masken für ursprungsübergreifende iFrames können falsch negative Ergebnisse gemeldet werden. isInputPending() kann beispielsweise beim Targeting auf diese Frames unerwartet „false“ zurückgeben. Achten Sie darauf, dass Sie die Funktion „Yield“ häufig genug verwenden, wenn Ihre Website Interaktionen mit stilisierten Subframes erfordert.

Berücksichtigen Sie auch andere Seiten, die eine Ereignisschleife teilen. Auf Plattformen wie Chrome für Android ist es durchaus üblich, dass mehrere Ursprünge eine Ereignisschleife teilen. isInputPending() gibt nie true zurück, wenn Eingabe an einen Frame mit unterschiedlichen Ursprüngen gesendet wird. Daher können Hintergrundseiten die Reaktionsfähigkeit von Vordergrundseiten beeinträchtigen. Wenn Sie im Hintergrund mit der Page Visibility API arbeiten, können Sie die Anzahl der Arbeitsschritte aufschieben, aufschieben oder häufiger erzielen.

Wir empfehlen, isInputPending() mit Bedacht zu verwenden. Wenn keine Arbeit ansteht, die die Ausführung des Codes für den Nutzer blockiert, sollten Sie anderen im Event-Loop entgegenkommen und häufiger zurückgeben. 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 Spezifikationsautoren.

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 zu diesem Punkt verholfen haben. Ein besonderer Dank geht an alle Mitarbeiter von Chrome, die uns dabei geholfen haben, diese Idee zu entwickeln und umzusetzen.

Hero-Foto von Will H McMahan auf Unsplash.