Jetzt neu: Ursprungstest „scheduler.yield“

Das Erstellen von Websites, die schnell auf Nutzereingaben reagieren, ist einer der schwierigsten Aspekte der Webleistung. Das Chrome-Team arbeitet hart daran, Webentwicklern dabei zu helfen. Erst in diesem Jahr wurde angekündigt, dass der Messwert „Interaction to Next Paint“ (INP) von „experimentell“ in den Status „Ausstehend“ wechselt. Er wird nun im März 2024 First Input Delay (FID) als Core Web Vital ersetzen.

Das Chrome-Team arbeitet kontinuierlich daran, neue APIs zur Verfügung zu stellen, mit denen Webentwickler ihre Websites so schnell wie möglich gestalten können. Deshalb führt das Chrome-Team derzeit einen Ursprungstest für scheduler.yield aus, der in Chrome-Version 115 beginnt. scheduler.yield ist eine vorgeschlagene neue Ergänzung zur Scheduler API, die eine einfachere und bessere Möglichkeit bietet, die Kontrolle zurück zum Hauptthread zu erhalten als die bisher genutzten Methoden.

Beim Ertrag

JavaScript verwendet das Modell für die vollständige Ausführung, um Aufgaben zu erledigen. Wenn also eine Aufgabe im Hauptthread ausgeführt wird, wird sie so lange ausgeführt, wie für ihren Abschluss erforderlich ist. Nach Abschluss einer Aufgabe wird die Steuerung an den Hauptthread (yield) zurückgegeben. Dadurch kann der Hauptthread die nächste Aufgabe in der Warteschlange verarbeiten.

Abgesehen von extremen Fällen, in denen eine Aufgabe nie beendet wird, wie z. B. bei einer Endlosschleife, ist die Erreichung ein unvermeidlicher Aspekt der JavaScript-Logik für die Aufgabenplanung. Es wird passieren, es kommt nur darauf an, wann? Früher ist besser als später. Wenn die Ausführung von Aufgaben zu lange dauert – mehr als 50 Millisekunden, um genau zu sein –, werden sie als lange Aufgaben betrachtet.

Lange Aufgaben führen zu einer schlechten Reaktionsfähigkeit von Seiten, da sie die Reaktion des Browsers auf Nutzereingaben verzögern. Je häufiger lange Aufgaben auftreten – und je länger sie ausgeführt werden, desto wahrscheinlicher ist es, dass Nutzer den Eindruck bekommen, dass die Seite träge oder sogar defekt ist.

Nur weil Ihr Code eine Aufgabe im Browser startet, bedeutet dies nicht, dass Sie warten müssen, bis die Aufgabe abgeschlossen ist, bevor die Steuerung wieder an den Hauptthread zurückgegeben wird. Sie können die Reaktionsfähigkeit auf Nutzereingaben auf einer Seite verbessern, indem Sie explizit zu einer Aufgabe liefern, wodurch die Aufgabe so unterbrochen wird, dass sie bei der nächsten verfügbaren Gelegenheit abgeschlossen wird. So erhalten andere Aufgaben schneller Zeit im Hauptthread, als wenn sie auf den Abschluss langer Aufgaben warten müssten.

Darstellung, wie das Aufteilen einer Aufgabe zu einer besseren Reaktion auf Eingaben beitragen kann. Oben wird die Ausführung eines Event-Handlers durch eine lange Aufgabe blockiert, bis die Aufgabe abgeschlossen ist. Unten lässt die aufgeteilte Aufgabe dem Event-Handler zu, früher als sonst ausgeführt zu werden.
Visualisierung der Steuerung zurück an den Hauptthread. Im Grunde erfolgt die Ausgabe erst, nachdem eine Aufgabe vollständig ausgeführt wurde. Das bedeutet, dass die Ausführung von Aufgaben länger dauern kann, bevor die Steuerung an den Hauptthread zurückgegeben wird. Unten wird das Ergebnis explizit ausgeführt, wobei eine lange Aufgabe in mehrere kleinere Aufgaben aufgeteilt wird. Dadurch können Nutzerinteraktionen früher ausgeführt werden, was die Eingabereaktionsfähigkeit und INP verbessert.

Wenn Sie explizit etwas zurückgeben, sagen Sie dem Browser: „Ich verstehe, dass die Arbeit, die ich jetzt erledige, eine Weile in Anspruch nehmen kann. Ich möchte nicht, dass Sie alle Aufgaben erledigen müssen, bevor Sie auf Nutzereingaben oder andere Aufgaben reagieren, die ebenfalls wichtig sein könnten.“ Es ist ein wertvolles Tool in der Toolbox eines Entwicklers, das maßgeblich dazu beitragen kann, die User Experience zu verbessern.

Das Problem mit den aktuellen Ertragsstrategien

Eine gängige Methode zum Abrufen verwendet setTimeout mit einem Zeitlimitwert von 0. Dies funktioniert, da der an setTimeout übergebene Callback die verbleibende Arbeit in eine separate Aufgabe verschiebt, die zur nachfolgenden Ausführung in die Warteschlange gestellt wird. Anstatt darauf zu warten, dass der Browser von alleine rentiert, sagen Sie: „Lassen Sie uns diesen großen Arbeitsblock in kleinere Teile aufteilen“.

Das Ergebnis mit setTimeout hat jedoch einen potenziell unerwünschten Nebeneffekt: Die Arbeit, die nach dem Ertragspunkt folgt, 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 ausführen möchten, nachdem sie explizit ausgegeben wurden, könnte jedoch durch andere Aufgaben aus konkurrierenden Quellen, die davor in die Warteschlange gestellt wurden, weiter verzögert werden.

In dieser Glitch-Demo kannst du dies in Aktion sehen. Du kannst auch unten in der eingebetteten Version mit dem Test experimentieren. Die Demo besteht aus einigen Schaltflächen, auf die Sie klicken können, und einem Feld darunter, das die Ausführung von Aufgaben protokolliert. Führen Sie auf der Seite folgende Aktionen aus:

  1. Klicken Sie auf die oberste Schaltfläche mit der Bezeichnung Aufgaben regelmäßig ausführen. Dadurch werden blockierende Aufgaben regelmäßig ausgeführt. Wenn Sie auf diese Schaltfläche klicken, wird das Aufgabenprotokoll mit mehreren Meldungen gefüllt, in denen die Meldung Ran-Blocking Task mit setInterval angezeigt wird.
  2. Klicken Sie als Nächstes auf die Schaltfläche Schleife ausführen, mit setTimeout bei jedem Durchlauf.

Sie werden feststellen, dass das Feld am unteren Rand der Demo in etwa so aussieht:

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 am Ende der Aufgabenwarteschlange, das auftritt, wenn die Ausgabe mit setTimeout erfolgt. Die Schleife, die fünf Artikel ausführt, und gibt setTimeout zurück, nachdem jedes Element verarbeitet wurde.

Dies veranschaulicht ein häufiges Problem im Web: Es ist nicht ungewöhnlich, dass ein Skript – insbesondere Skripts von Drittanbietern – eine Timer-Funktion registriert, die in bestimmten Intervallen ausgeführt wird. Das Verhalten "Ende der Aufgabenwarteschlange", das mit der Ausgabe von setTimeout einhergeht, bedeutet, dass Arbeit aus anderen Aufgabenquellen vor der verbleibenden Arbeit, die die Schleife nach der Ausgabe erledigen muss, in die Warteschlange gestellt werden kann.

Abhängig von Ihrer Anwendung kann dies ein wünschenswertes Ergebnis sein oder nicht. In vielen Fällen ist dieses Verhalten jedoch der Grund dafür, dass Entwickler zögern, die Kontrolle über den Hauptthread so schnell aufzugeben. Die Erzielung von Ergebnissen ist gut, weil Nutzerinteraktionen früher ausgeführt werden können, aber auch andere Aufgaben, die nicht mit dem Nutzer interagieren, Zeit im Hauptthread erhalten. Es ist ein echtes Problem, aber scheduler.yield kann dir helfen, es zu lösen.

scheduler.yield eingeben

scheduler.yield steht seit Chrome-Version 115 hinter einem Flag als experimentelle Webplattformfunktion zur Verfügung. Eine Frage, die Sie möglicherweise haben könnten, lautet: „Warum benötige ich eine spezielle Funktion, um zu liefern, wenn setTimeout sie bereits tut?“

Das Ergebnis war nicht ein Designziel von setTimeout, sondern ein positiver Nebeneffekt bei der Planung eines Callbacks, der später in der Zukunft ausgeführt wird – auch wenn ein Zeitlimit von 0 angegeben ist. Beachten Sie jedoch, dass die verbleibende Arbeit, die mit setTimeout erzielt wird, hinter der Aufgabenwarteschlange steht. Standardmäßig schickt scheduler.yield die verbleibende Arbeit an den Vorderseite der Warteschlange. Das bedeutet, dass die Arbeit, die Sie sofort nach Erledigung fortsetzen wollten, nicht den Rücken zu Aufgaben aus anderen Quellen verschiebe (mit Ausnahme von Benutzerinteraktionen).

scheduler.yield ist eine Funktion, die etwas an den Hauptthread liefert und bei Aufruf ein Promise zurückgibt. Das heißt, Sie können ihn in einer async-Funktion await:

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 Webplattformfunktionen. Möglicherweise müssen Sie danach Chrome neu starten.
  3. Rufen Sie die Demoseite auf oder verwenden Sie die eingebettete Version unter dieser Liste.
  4. Klicken Sie auf die oberste Schaltfläche mit der Bezeichnung Aufgaben regelmäßig ausführen.
  5. Klicken Sie abschließend auf die Schaltfläche Run Interesse (Schleife ausführen) und erhalten bei jedem Durchlauf scheduler.yield.

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 mit setTimeout-Ergebnissen können Sie sehen, dass die Schleife – auch wenn sie nach jeder Iteration zurückgegeben wird – die verbleibende Arbeit nicht an den Anfang der Warteschlange stellt, sondern an den Anfang der Warteschlange gestellt wird. So profitieren Sie von den Vorteilen beider Welten: Sie können die Reaktionsfähigkeit von Eingaben auf Ihrer Website verbessern und gleichzeitig dafür sorgen, dass die Arbeit, die Sie nach der Bereitstellung erledigen wollten, nicht verzögert wird.

Probier die Funktion einfach aus!

Wenn scheduler.yield interessant für dich aussieht und du es ausprobieren möchtest, hast du ab Version 115 von Chrome 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 Bereich Experimental Web Platform Features aus dem Drop-down-Menü die Option Aktivieren aus. Dadurch werden scheduler.yield und alle anderen experimentellen Funktionen nur in Ihrer Instanz von Chrome verfügbar gemacht.
  2. Wenn du scheduler.yield für echte Chromium-Nutzer auf einer öffentlich zugänglichen Quelle aktivieren möchtest, musst du dich für den scheduler.yield-Ursprungstest registrieren. So können Sie vorgeschlagene Funktionen in einem bestimmten Zeitraum sicher ausprobieren und das Chrome-Team wertvolle Erkenntnisse darüber gewinnen, wie diese Funktionen vor Ort eingesetzt werden. Weitere Informationen zur Funktionsweise von Ursprungstests finden Sie in diesem Leitfaden.

Wie Sie scheduler.yield verwenden und gleichzeitig Browser unterstützen, die die Funktion nicht implementieren, hängt von Ihren Zielen ab. Du kannst den offiziellen Polyfill verwenden. Polyfill ist nützlich, wenn Folgendes auf deine Situation zutrifft:

  1. Sie verwenden in Ihrer Anwendung bereits scheduler.postTask, um Aufgaben zu planen.
  2. Sie möchten Aufgaben festlegen und Prioritäten setzen.
  3. Sie möchten Aufgaben über die Klasse TaskController, die die API scheduler.postTask bietet, stornieren oder neu zuweisen.

Wenn dies nicht auf deine Situation zutrifft, ist der Polyfill nicht für dich geeignet. In diesem Fall haben Sie mehrere Möglichkeiten, für Ihr eigenes Fallback ein Rollback durchzuführen. Beim ersten Ansatz wird scheduler.yield verwendet, wenn der Wert verfügbar ist. Ist dies nicht der Fall, 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 vielleicht denken können, erzielen Browser, die scheduler.yield nicht unterstützen, kein „Front-of-Queue“-Verhalten. Wenn Sie in diesem Fall überhaupt keine Ergebnisse erzielen möchten, können Sie eine andere Methode ausprobieren, bei der scheduler.yield verwendet wird, sofern verfügbar, aber andernfalls überhaupt keine Ergebnisse liefern:

// 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 erleichtern wird, die Reaktionszeit zu verbessern als mit aktuellen Ertragsgruppen. Wenn scheduler.yield Ihnen eine nützliche API zu sein scheint, nehmen Sie bitte an unserer Studie teil, damit wir sie verbessern können, und geben Sie uns Feedback dazu, was wir noch verbessern können.

Hero-Image aus Unsplash von Jonathan Allison