Jetzt neu: Ursprungstest „scheduler.yield“

Websites zu entwickeln, die schnell auf Nutzereingaben reagieren, ist einer der schwierigsten Aspekte der Webleistung. Das Chrome-Team arbeitet intensiv daran, Webentwicklern dabei zu helfen, diese Herausforderung zu meistern. Erst in diesem Jahr wurde angekündigt, dass der Messwert „Interaction to Next Paint“ (INP) den Status „Experimentell“ verliert und in den Status „Ausstehend“ übergeht. Im März 2024 wird er First Input Delay (FID) als Core Web Vital ersetzen.

Das Chrome-Team arbeitet kontinuierlich daran, neue APIs bereitzustellen, mit denen Webentwickler ihre Websites so flüssig wie möglich gestalten können. Derzeit führt das Team einen Ursprungstest für scheduler.yield durch, der ab Chrome-Version 115 verfügbar ist. scheduler.yield ist ein vorgeschlagener neuer Bestandteil der Scheduler API, der eine einfachere und bessere Möglichkeit bietet, die Kontrolle an den Hauptthread zurückzugeben als die bisher verwendeten Methoden.

Beim Ausweichen

In JavaScript werden Aufgaben mit dem Run-to-Completion-Modell ausgeführt. Wenn also eine Aufgabe im Hauptthread ausgeführt wird, dauert dies so lange, wie es für die Ausführung erforderlich ist. Nach Abschluss einer Aufgabe wird die Steuerung an den Hauptthread zurückgegeben, damit dieser die nächste Aufgabe in der Warteschlange verarbeiten kann.

Abgesehen von extremen Fällen, in denen eine Aufgabe nie abgeschlossen wird, z. B. bei einer Endlosschleife, ist das Ausgeben ein unvermeidlicher Aspekt der Task-Planungslogik von JavaScript. Es wird passieren, es ist nur eine Frage des Wanns. Je früher, desto besser. Wenn Aufgaben zu lange dauern, genauer gesagt mehr als 50 Millisekunden, werden sie als lange Aufgaben eingestuft.

Lange Aufgaben beeinträchtigen die Reaktionsfähigkeit der Seite, da sie die Reaktion des Browsers auf Nutzereingaben verzögern. Je häufiger lange Aufgaben auftreten und je länger sie laufen, desto wahrscheinlicher ist es, dass Nutzer den Eindruck bekommen, dass die Seite träge ist oder sogar ganz kaputt ist.

Nur weil Ihr Code eine Aufgabe im Browser startet, bedeutet das nicht, dass Sie warten müssen, bis diese Aufgabe abgeschlossen ist, bevor die Kontrolle an den Hauptthread zurückgegeben wird. Sie können die Reaktion auf Nutzereingaben auf einer Seite verbessern, indem Sie bei einer Aufgabe explizit einlenken. Dadurch wird die Aufgabe in mehrere Teile aufgeteilt, die bei der nächsten verfügbaren Gelegenheit abgeschlossen werden. So können andere Aufgaben früher Zeit im Hauptthread erhalten, als wenn sie auf das Ende langer Aufgaben warten müssten.

Eine Darstellung, wie die Aufteilung einer Aufgabe zu einer besseren Reaktion auf Eingaben führen kann. Oben blockiert eine lange Aufgabe die Ausführung eines Ereignis-Handlers, bis die Aufgabe abgeschlossen ist. Unten wird durch die in kleinere Teile aufgeteilte Aufgabe ermöglicht, dass der Ereignishandler früher ausgeführt wird als sonst.
Visualisierung der Rückgabe der Kontrolle an den Hauptthread. Oben wird die Übergabe erst nach Abschluss einer Aufgabe ausgeführt. Das bedeutet, dass Aufgaben länger dauern können, bis die Kontrolle an den Hauptthread zurückgegeben wird. Im Grunde wird Yielding explizit ausgeführt, indem eine lange Aufgabe in mehrere kleinere Aufgaben aufgeteilt wird. So können Nutzerinteraktionen früher ausgeführt werden, was die Eingabereaktion und die INP verbessert.

Wenn Sie explizit einlenken, teilen Sie dem Browser mit: „Ich weiß, dass die Arbeit, die ich jetzt erledigen werde, etwas dauern kann. Ich möchte nicht, dass Sie alles erledigen müssen, bevor Sie auf Nutzereingaben oder andere Aufgaben reagieren, die auch wichtig sein könnten.“ Es ist ein wertvolles Tool in der Toolbox eines Entwicklers, mit dem sich die Nutzerfreundlichkeit erheblich verbessern lässt.

Das Problem mit aktuellen Strategien zur Erzielung von Einnahmen

Eine gängige Methode zum Erzielen von ist die Verwendung von setTimeout mit einem Zeitüberschreitungswert von 0. Das funktioniert, weil der an setTimeout übergebene Rückruf die verbleibende Arbeit in eine separate Aufgabe verschiebt, die für die nachfolgende Ausführung in die Warteschlange gestellt wird. Anstatt darauf zu warten, dass der Browser von selbst reagiert, sagen Sie: „Lass uns diesen großen Arbeitsblock in kleinere Teile aufteilen.“

Das Ausgeben von setTimeout hat jedoch einen potenziell unerwünschten Nebeneffekt: Die Arbeit, die nach dem Yield-Punkt kommt, wird an das Ende der Aufgabenwarteschlange verschoben. Aufgaben, die durch Nutzerinteraktionen geplant wurden, werden weiterhin wie vorgesehen an den Anfang der Warteschlange gestellt. Die verbleibende Arbeit, die Sie nach dem expliziten Aussetzen erledigen wollten, kann jedoch durch andere Aufgaben aus konkurrierenden Quellen, die vor ihr in die Warteschlange gestellt wurden, weiter verzögert werden.

Diese Glitch-Demo oder die unten eingebettete Version veranschaulichen das Prinzip. Die Demo besteht aus einigen Schaltflächen, auf die Sie klicken können, und einem Feld darunter, in dem protokolliert wird, wann Aufgaben ausgeführt werden. Führen Sie auf der Seite die folgenden Aktionen aus:

  1. Klicken Sie oben auf die Schaltfläche Aufgaben regelmäßig ausführen. Dadurch werden blockierende Aufgaben in regelmäßigen Abständen ausgeführt. Wenn Sie auf diese Schaltfläche klicken, werden im Aufgabenprotokoll mehrere Meldungen mit dem Text Blockierungsaufgabe mit setInterval ausgeführt angezeigt.
  2. Klicken Sie dann auf die Schaltfläche Schleife ausführen und bei jeder Iteration setTimeout zurückgeben.

Im Feld unten in der Demo wird ungefähr Folgendes angezeigt:

Processing loop item 1
Processing loop item 2
Ran blocking task via setInterval
Processing loop item 3
Ran blocking task via setInterval
Processing loop item 4
Ran blocking task via setInterval
Processing loop item 5
Ran blocking task via setInterval
Ran blocking task via setInterval

Diese Ausgabe zeigt das Verhalten „Ende der Aufgabenwarteschlange“, das auftritt, wenn mit setTimeout ein Yield ausgeführt wird. In der Schleife werden fünf Elemente verarbeitet und nach jeder Verarbeitung wird setTimeout ausgegeben.

Das veranschaulicht ein häufiges Problem im Web: Es ist nicht ungewöhnlich, dass ein Script – insbesondere ein Script von Drittanbietern – eine Timerfunktion registriert, die in einem bestimmten Intervall ausgeführt wird. Das Verhalten „Ende der Aufgabenwarteschlange“, das beim Ausgeben mit setTimeout auftritt, bedeutet, dass Aufgaben aus anderen Aufgabenquellen vor der verbleibenden Arbeit in die Warteschlange gestellt werden, die die Schleife nach dem Ausgeben ausführen muss.

Je nach Anwendung kann dies ein gewünschtes Ergebnis sein oder auch nicht. In vielen Fällen ist dieses Verhalten jedoch der Grund, warum Entwickler die Kontrolle über den Hauptthread nicht so leichtfertig abgeben. Das Ausgeben ist gut, weil Nutzerinteraktionen so früher ausgeführt werden können. Außerdem kann so auch andere Arbeit, die nicht mit Nutzerinteraktionen zusammenhängt, Zeit im Hauptthread erhalten. Das ist ein echtes Problem, aber scheduler.yield kann Ihnen dabei helfen, es zu lösen.

scheduler.yield eingeben

scheduler.yield ist seit Version 115 von Chrome als experimentelle Webplattformfunktion verfügbar. Sie könnten sich fragen: „Warum brauche ich eine spezielle Funktion für die Ausgabe, wenn setTimeout das bereits tut?“

Das Ausgeben war kein Designziel von setTimeout, sondern eher ein angenehmer Nebeneffekt beim Planen eines Callbacks, der zu einem späteren Zeitpunkt ausgeführt werden soll – auch wenn ein Zeitüberschreitungswert von 0 angegeben wurde. Wichtiger ist jedoch, dass verbleibende Arbeit durch Yielding mit setTimeout an das Ende der Aufgabenwarteschlange gesendet wird. Standardmäßig sendet scheduler.yield verbleibende Arbeit an den Anfang der Warteschlange. Das bedeutet, dass Aufgaben, die Sie nach dem Ausführen fortsetzen wollten, nicht von Aufgaben aus anderen Quellen verdrängt werden (mit der bemerkenswerten Ausnahme von Nutzerinteraktionen).

scheduler.yield ist eine Funktion, die den Hauptthread anweist, die Ausführung zu beenden, und beim Aufrufen einen Promise zurückgibt. Sie können sie also await in einer async-Funktion verwenden:

async function yieldy () {
  // Do some work...
  // ...

  // Yield!
  await scheduler.yield();

  // Do some more work...
  // ...
}

So sehen Sie sich scheduler.yield in Aktion an:

  1. Rufen Sie chrome://flags auf.
  2. Aktivieren Sie den Test Experimentelle Funktionen der Webplattform. Möglicherweise müssen Sie Chrome danach neu starten.
  3. Rufen Sie die Demoseite auf oder verwenden Sie die eingebettete Version unten in dieser Liste.
  4. Klicken Sie oben auf die Schaltfläche Aufgaben regelmäßig ausführen.
  5. Klicken Sie abschließend auf die Schaltfläche Schleife ausführen und bei jeder Iteration mit scheduler.yield enden.

Die Ausgabe im Feld unten auf der Seite sieht in etwa so aus:

Processing loop item 1
Processing loop item 2
Processing loop item 3
Processing loop item 4
Processing loop item 5
Ran blocking task via setInterval
Ran blocking task via setInterval
Ran blocking task via setInterval
Ran blocking task via setInterval
Ran blocking task via setInterval

Im Gegensatz zur Demo, in der mit setTimeout gewartet wird, sehen Sie hier, dass die Schleife die verbleibende Arbeit nicht an das Ende der Warteschlange sendet, sondern an den Anfang. So haben Sie das Beste aus beiden Welten: Sie können die Eingabereaktion auf Ihrer Website verbessern, aber auch dafür sorgen, dass die Arbeit, die Sie nach dem Yielding erledigen wollten, nicht verzögert wird.

Probieren Sie es aus!

Wenn scheduler.yield für Sie interessant ist und Sie es ausprobieren möchten, haben Sie ab Version 115 von Chrome zwei Möglichkeiten:

  1. Wenn Sie scheduler.yield lokal ausprobieren möchten, geben Sie chrome://flags in die Adressleiste von Chrome ein und wählen Sie im Drop-down-Menü unter Experimentelle Webplattformfunktionen die Option Aktivieren aus. scheduler.yield und alle anderen experimentellen Funktionen sind dann nur in Ihrer Chrome-Instanz verfügbar.
  2. Wenn Sie scheduler.yield für echte Chromium-Nutzer auf einem öffentlich zugänglichen Ursprung aktivieren möchten, müssen Sie sich für den scheduler.yield-Ursprungstest registrieren. So können Sie für einen bestimmten Zeitraum sicher mit vorgeschlagenen Funktionen experimentieren und das Chrome-Team erhält wertvolle Einblicke in die praktische Nutzung dieser Funktionen. Weitere Informationen zur Funktionsweise von Ursprungstests finden Sie in diesem Leitfaden.

Wie Sie scheduler.yield verwenden, um auch Browser zu unterstützen, die es nicht implementieren, hängt von Ihren Zielen ab. Sie können die offizielle polyfill verwenden. Die polyfill ist nützlich, wenn Folgendes zutrifft:

  1. Sie verwenden bereits scheduler.postTask in Ihrer Anwendung, um Aufgaben zu planen.
  2. Sie möchten Aufgaben und Erträge priorisieren können.
  3. Sie möchten Aufgaben über die TaskController-Klasse der scheduler.postTask API abbrechen oder neu priorisieren können.

Wenn dies nicht auf Ihre Situation zutrifft, ist die Polyfill möglicherweise nicht für Sie geeignet. In diesem Fall haben Sie mehrere Möglichkeiten, einen eigenen Fallback einzurichten. Beim ersten Ansatz wird scheduler.yield verwendet, wenn es verfügbar ist. Andernfalls wird setTimeout verwendet:

// A function for shimming scheduler.yield and setTimeout:
function yieldToMain () {
  // Use scheduler.yield if it exists:
  if ('scheduler' in window && 'yield' in scheduler) {
    return scheduler.yield();
  }

  // Fall back to setTimeout:
  return new Promise(resolve => {
    setTimeout(resolve, 0);
  });
}

// Example usage:
async function doWork () {
  // Do some work:
  // ...

  await yieldToMain();

  // Do some other work:
  // ...
}

Das kann funktionieren, aber wie Sie sich vorstellen können, werden Browser, die scheduler.yield nicht unterstützen, nicht an den Anfang der Warteschlange gestellt. Wenn Sie lieber gar keine Ergebnisse zurückgeben möchten, können Sie einen anderen Ansatz ausprobieren, bei dem scheduler.yield verwendet wird, wenn es verfügbar ist, aber keine Ergebnisse zurückgegeben werden, wenn es nicht verfügbar ist:

// A function for shimming scheduler.yield with no fallback:
function yieldToMain () {
  // Use scheduler.yield if it exists:
  if ('scheduler' in window && 'yield' in scheduler) {
    return scheduler.yield();
  }

  // Fall back to nothing:
  return;
}

// Example usage:
async function doWork () {
  // Do some work:
  // ...

  await yieldToMain();

  // Do some other work:
  // ...
}

scheduler.yield ist eine spannende Ergänzung der Scheduler API, die es Entwicklern hoffentlich leichter machen wird, die Reaktionsfähigkeit zu verbessern als mit aktuellen Yielding-Strategien. Wenn Sie der Meinung sind, dass scheduler.yield eine nützliche API ist, nehmen Sie bitte an unseren Studien teil, um uns bei der Verbesserung zu unterstützen. Geben Sie uns Feedback, wie sie noch verbessert werden könnte.

Hero-Image von Unsplash, von Jonathan Allison.