Jetzt neu: Ursprungstest „scheduler.yield“

Die Entwicklung von Websites, die schnell auf Nutzereingaben reagieren, ist einer der anspruchsvollsten Aspekte der Webleistung. Das Chrome-Team arbeitet intensiv daran, Webentwicklern dabei zu helfen. Erst in diesem Jahr wurde angekündigt, dass der Messwert „Interaction to Next Paint“ (INP) vom experimentellen in den ausstehenden Status übergeht. Im März 2024 soll First Input Delay (FID) als Core Web Vital ersetzt werden.

Das Chrome-Team arbeitet kontinuierlich daran, neue APIs bereitzustellen, mit denen Webentwickler ihre Websites so schnell wie möglich gestalten können. Daher wird ab Chrome 115 ein Ursprungstest für scheduler.yield durchgeführt. scheduler.yield ist ein vorgeschlagener neuer Zusatz zur Scheduler API, der sowohl eine einfachere als auch eine bessere Möglichkeit bietet, die Steuerung an den Hauptthread zurückzugeben als die Methoden, auf die traditionell zurückgegriffen wurde.

Bei der Auslieferung

JavaScript verwendet das Run-to-Completion-Modell für die Verarbeitung von Aufgaben. Wenn eine Aufgabe im Hauptthread ausgeführt wird, wird sie so lange ausgeführt, bis sie abgeschlossen ist. Nach Abschluss einer Aufgabe wird die Steuerung wieder an den Hauptthread übergeben, sodass der Hauptthread die nächste Aufgabe in der Warteschlange verarbeiten kann.

Abgesehen von Extremfällen, in denen eine Aufgabe nie abgeschlossen wird, z. B. bei einer Endlosschleife, ist das Yielding ein unvermeidlicher Aspekt der Logik für die Aufgabenplanung von JavaScript. Es wird passieren, es ist nur eine Frage des Wann, und früher ist besser als später. Wenn die Ausführung von Aufgaben zu lange dauert, genauer gesagt länger als 50 Millisekunden, gelten sie als Long Tasks.

Lange Tasks sind eine Quelle für eine schlechte Reaktionsfähigkeit der Seite, da sie die Fähigkeit des Browsers verzögern, auf Nutzereingaben zu reagieren. Je häufiger und länger langwierige Aufgaben ausgeführt werden, desto wahrscheinlicher ist es, dass Nutzer den Eindruck haben, die Seite sei träge oder sogar defekt.

Nur weil Ihr Code eine Aufgabe im Browser startet, müssen Sie jedoch nicht warten, bis diese Aufgabe abgeschlossen ist, bevor die Steuerung an den Hauptthread zurückgegeben wird. Sie können die Reaktionsschnelligkeit auf Nutzereingaben auf einer Seite verbessern, indem Sie in einer Aufgabe explizit nachgeben. Dadurch wird die Aufgabe unterbrochen und kann bei der nächsten verfügbaren Gelegenheit abgeschlossen werden. Dadurch können andere Aufgaben früher im Hauptthread ausgeführt werden, als wenn sie auf den Abschluss langer Aufgaben warten müssten.

Eine Darstellung, wie das Aufteilen einer Aufgabe die Reaktionsfähigkeit bei der Eingabe verbessern kann. Oben blockiert eine lange Aufgabe die Ausführung eines Ereignis-Handlers, bis die Aufgabe abgeschlossen ist. Unten ermöglicht die aufgeteilte Aufgabe, dass der Event-Handler früher ausgeführt wird als sonst.
Eine Visualisierung der Rückgabe der Steuerung an den Hauptthread. Im oberen Bereich erfolgt die Übergabe erst, nachdem eine Aufgabe abgeschlossen wurde. Das bedeutet, dass es länger dauern kann, bis Aufgaben abgeschlossen sind, bevor die Steuerung an den Hauptthread zurückgegeben wird. Im unteren Beispiel wird die Ausführung explizit unterbrochen, um eine lange Aufgabe in mehrere kleinere Aufgaben aufzuteilen. So können Nutzerinteraktionen früher ausgeführt werden, was die Reaktionsschnelligkeit bei Eingaben und INP verbessert.

Wenn Sie explizit nachgeben, sagen Sie dem Browser: „Hey, ich weiß, dass die Arbeit, die ich gleich erledigen werde, eine Weile dauern könnte, und ich möchte nicht, dass du all diese Arbeit erledigen musst, bevor du auf Nutzereingaben oder andere wichtige Aufgaben reagierst.“ Es ist ein wertvolles Tool für Entwickler, mit dem sich die Nutzerfreundlichkeit erheblich verbessern lässt.

Das Problem mit aktuellen Ertragsstrategien

Eine gängige Methode zum Yielding von verwendet setTimeout mit einem Zeitüberschreitungswert von 0. Das funktioniert, weil durch den an setTimeout übergebenen Callback die verbleibende Arbeit in eine separate Aufgabe verschoben wird, die für die spätere Ausführung in die Warteschlange gestellt wird. Anstatt darauf zu warten, dass der Browser von selbst nachgibt, sagen Sie: „Teile diesen großen Arbeitsblock in kleinere Teile auf.“

Das Yielding mit setTimeout hat jedoch eine potenziell unerwünschte Nebenwirkung: Die Arbeit, die nach dem Yield-Punkt erfolgt, wird am Ende der Aufgabenwarteschlange ausgeführt. Aufgaben, die durch Nutzerinteraktionen geplant werden, werden weiterhin wie vorgesehen an den Anfang der Warteschlange gestellt. Die verbleibende Arbeit, die Sie nach dem expliziten Yielding ausführen wollten, kann jedoch durch andere Aufgaben aus konkurrierenden Quellen, die vor ihr in die Warteschlange gestellt wurden, weiter verzögert werden.

In dieser Codepen-Demo können Sie sich das in Aktion ansehen. Sie können auch mit der folgenden eingebetteten Version experimentieren. Die Demo besteht aus einigen Schaltflächen, auf die Sie klicken können, und einem Feld darunter, in dem protokolliert wird, wenn Aufgaben ausgeführt werden. Führen Sie auf der Seite folgende Aktionen aus:

  1. Klicken Sie auf die obere Schaltfläche mit der Aufschrift Aufgaben regelmäßig ausführen. Dadurch werden blockierende Aufgaben so geplant, dass sie in regelmäßigen Abständen ausgeführt werden. Wenn Sie auf diese Schaltfläche klicken, wird das Aufgabenprotokoll mit mehreren Meldungen gefüllt, in denen Blockierende Aufgabe mit setInterval ausgeführt steht.
  2. Klicken Sie als Nächstes auf die Schaltfläche Run loop, yielding with setTimeout on each iteration (Schleife ausführen, bei jeder Iteration mit setTimeout zurückgeben).

Im Feld unten in der Demo steht dann etwa Folgendes:

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 beim Yielding mit setTimeout auftritt. Die Schleife verarbeitet fünf Elemente und gibt nach jedem Element setTimeout aus.

Das ist ein häufiges Problem im Web: Es ist nicht ungewöhnlich, dass ein Script, insbesondere ein Drittanbieter-Script, eine Timerfunktion registriert, die in einem bestimmten Intervall ausgeführt wird. Das Verhalten „Ende der Aufgabenwarteschlange“, das mit der Übergabe mit setTimeout einhergeht, bedeutet, dass Aufgaben aus anderen Aufgabenquellen vor den verbleibenden Aufgaben, die die Schleife nach der Übergabe ausführen muss, in die Warteschlange gestellt werden können.

Je nach Anwendung kann dies ein wünschenswertes Ergebnis sein oder nicht. In vielen Fällen ist dieses Verhalten der Grund, warum Entwickler zögern, die Kontrolle über den Hauptthread so schnell aufzugeben. Das Yielding ist gut, weil Nutzerinteraktionen früher ausgeführt werden können. Außerdem können auch andere Aufgaben, die nicht mit Nutzerinteraktionen zusammenhängen, auf dem Hauptthread ausgeführt werden. Das ist ein echtes Problem – aber scheduler.yield kann helfen, es zu lösen.

scheduler.yield eingeben

scheduler.yield ist seit Chrome-Version 115 als experimentelle Webplattformfunktion hinter einem Flag verfügbar. Eine Frage, die Sie sich vielleicht stellen, ist: „Warum brauche ich eine spezielle Funktion für die Übergabe, wenn setTimeout das bereits erledigt?“

Es ist wichtig zu beachten, dass die Übergabe kein Designziel von setTimeout war, sondern eher ein angenehmer Nebeneffekt bei der Planung eines Rückrufs, der zu einem späteren Zeitpunkt ausgeführt werden soll – auch wenn ein Zeitüberschreitungswert von 0 angegeben ist. Wichtiger ist jedoch, dass durch die Übergabe mit setTimeout die verbleibende Arbeit 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 unmittelbar nach dem Yielding fortsetzen möchten, nicht hinter Aufgaben aus anderen Quellen (mit Ausnahme von Nutzerinteraktionen) zurückgestellt werden.

scheduler.yield ist eine Funktion, die den Hauptthread freigibt und bei Aufruf ein Promise zurückgibt. Sie können await also in einer async-Funktion verwenden:

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

  // Yield!
  await scheduler.yield();

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

So sehen Sie scheduler.yield in Aktion:

  1. Rufen Sie chrome://flags auf.
  2. Aktivieren Sie den Test Experimentelle Webplattformfunktionen. Möglicherweise müssen Sie Chrome danach neu starten.
  3. Rufen Sie die Demoseite auf oder verwenden Sie die folgende eingebettete Version.
  4. Klicken Sie auf die obere Schaltfläche mit der Beschriftung Run tasks periodically (Aufgaben regelmäßig ausführen).
  5. Klicken Sie abschließend auf die Schaltfläche mit der Beschriftung Run loop, yielding with scheduler.yield on each iteration (Schleife ausführen, bei jeder Iteration mit scheduler.yield unterbrechen).

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, bei der setTimeout verwendet wird, wird die verbleibende Arbeit in der Schleife nicht ans Ende, sondern an den Anfang der Warteschlange verschoben, obwohl nach jeder Iteration ein Yield erfolgt. So können Sie die Reaktionsfähigkeit der Eingabe auf Ihrer Website verbessern, aber auch dafür sorgen, dass die Arbeit, die Sie nach dem Yielding erledigen möchten, 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 Chrome-Version 115 zwei Möglichkeiten:

  1. Wenn Sie scheduler.yield lokal testen möchten, geben Sie chrome://flags in die Adressleiste von Chrome ein und wählen Sie im Drop-down-Menü im Bereich Experimental Web Platform Features die Option Aktivieren aus. Dadurch wird scheduler.yield (und alle anderen experimentellen Funktionen) 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 vorgeschlagene Funktionen über einen bestimmten Zeitraum hinweg sicher testen und das Chrome-Team erhält wertvolle Informationen darüber, wie diese Funktionen in der Praxis verwendet werden. Weitere Informationen zu Origin Trials

Wie Sie scheduler.yield verwenden und gleichzeitig Browser unterstützen, die es nicht implementieren, hängt von Ihren Zielen ab. Sie können das offizielle Polyfill verwenden. Das Polyfill ist nützlich, wenn Folgendes auf Ihre Situation zutrifft:

  1. Sie verwenden scheduler.postTask bereits in Ihrer Anwendung, um Aufgaben zu planen.
  2. Sie möchten Prioritäten für Aufgaben und Yielding festlegen können.
  3. Sie möchten Aufgaben mit der TaskController-Klasse der scheduler.postTask API abbrechen oder neu priorisieren können.

Wenn das nicht auf Ihre Situation zutrifft, ist das Polyfill möglicherweise nicht für Sie geeignet. In diesem Fall können Sie auf verschiedene Arten einen eigenen Fallback erstellen. Beim ersten Ansatz wird scheduler.yield verwendet, sofern verfügbar. Andernfalls wird auf setTimeout zurückgegriffen:

// 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 vielleicht denken können, wird bei Browsern, die scheduler.yield nicht unterstützen, kein „Front of Queue“-Verhalten erzielt. Wenn Sie lieber gar keine Ertragsoptimierung durchführen möchten, können Sie einen anderen Ansatz verwenden, bei dem scheduler.yield genutzt wird, sofern verfügbar. Andernfalls wird keine Ertragsoptimierung durchgeführt:

// 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 interessante Ergänzung der Scheduler API, die es Entwicklern hoffentlich einfacher macht, die Reaktionsfähigkeit zu verbessern als mit den aktuellen Yielding-Strategien. Wenn scheduler.yield für Sie eine nützliche API ist, nehmen Sie bitte an unserer Studie teil, um sie zu verbessern, und geben Sie Feedback dazu, wie sie weiter verbessert werden könnte.

Hero-Image von Unsplash, von Jonathan Allison.