Eine neue JavaScript API, mit der Sie den Kompromiss zwischen Ladeleistung und Eingabereaktionsfähigkeit vermeiden können.
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
isInputPending()
ist in Chromium-basierten Browsern ab Version 87 verfügbar.
Kein anderer Browser hat die Absicht signalisiert, die API zu implementieren.
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 auf der Seite viel Arbeit ausgeführt wird, während ein Eingabeereignis ausgelöst wird, wird das Klickereignis beispielsweise erst nach Abschluss dieser Arbeit verarbeitet.
Die aktuelle Best Practice besteht darin, das JavaScript in kleinere Blöcke aufzuteilen. Während die Seite geladen wird, kann sie ein wenig JavaScript ausführen und dann die Kontrolle an den Browser zurückgeben. Der Browser kann dann seine Eingabeereigniswarteschlange prüfen und sehen, 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.
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.
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 wir in Chrome Änderungen testen und Feedback von Entwicklern einholen, 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: ein Yield-Optimierungstool
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. Jede dieser Aufgaben 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 noch 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 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();
}
Geschafft! 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 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 können beim Festlegen komplexer Clips und Masken für plattformübergreifende Iframes fälschlicherweise negative Ergebnisse zurückgegeben werden (d.h. isInputPending()
gibt beim Targeting auf diese Frames unerwartet „falsch“ zurück). 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 dieselbe Ereignisschleife verwenden. 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 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 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 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 zu diesem Punkt verholfen haben. Ein besonderer Dank geht an alle Mitarbeiter von Chrome, die uns dabei unterstützt haben, diese Idee zu entwickeln und umzusetzen.
Hero-Foto von Will H McMahan auf Unsplash.