setTimeout: Optimierung langer Aufgaben in JS

Michal MatuškaMichal MatuškaAktualisiert 11.2.20265 Minuten Lesen

Die Methode, die Funktion setTimeout() zu nutzen, dient der Optimierung langer Aufgaben in JavaScript (messbar durch die Metrik JS Long Tasks (JSLT)), die Benutzerinteraktionen blockieren und damit Metriken wie Total Blocking Time (TBT) oder Interaction to Next Paint (INP) verschlechtern, welche ein wesentlicher Bestandteil der Core Web Vitals sind.

Problem? Lange Aufgaben in JavaScript

JavaScript arbeitet im Browser nach dem Prinzip eines Einzel-Threads. In der Praxis bedeutet dies, dass alle Aktionen, Ereignisse und Interaktionen in Warteschlangen eingereiht werden, die von einem Mechanismus namens Event Loop gesteuert werden.

Aus der Perspektive der Webgeschwindigkeitsoptimierung entsteht ein Problem, wenn eine Aufgabe zu lange dauert und dadurch andere Aufgaben blockiert. Dies blockiert letztendlich den gesamten Browser und seinen sogenannten Main Thread. Diese langen Aufgaben werden durch die Metrik JS Long Tasks (JSLT) gemessen.

Lange Aufgaben in JS Lange Aufgaben dauern länger als 50 ms und sind beim Messen in den DevTools des Browsers durch eine rote Markierung erkennbar.

setTimeout() und die Optimierung von INP

Um hohe Werte der INP-Metrik zu optimieren, ist es notwendig, den Code nach Wichtigkeit zu trennen, weniger wichtige Teile zu verzögern und dadurch dem Browser Raum zu geben, um andere Benutzerinteraktionen zu verarbeiten.

Input received a long tasks Vor der Optimierung muss der Benutzer nach der Eingabe („Input received“) warten, bis die lange Aufgabe („Task“) ausgeführt wird, um die Aktion („Event“) zu starten. Nach der Optimierung wird die lange Aufgabe in kleinere aufgeteilt, sodass die Aktion schneller ausgeführt wird.

In JavaScript kann man mit langen Aufgaben umgehen, indem man sogenannte asynchrone Operationen verwendet. Dazu gehört das Timer-API, angeführt von der Funktion window.setTimeout(), die es ermöglicht, ausgewählte Aufgaben zu verschieben. Die Funktion setTimeout() hat zudem eine einzigartige Eigenschaft: Wenn eine Verzögerung von null gesetzt wird, wird der Browser die Aufgabe wahrscheinlich in den nächsten Rendering-Zyklus verschieben:

setTimeout(() => {
	kod_odlozeny_do_nasledujiciho_renderu();
}, 0);

Dank dieser Eigenschaft kann der Code relativ schnell aufgeteilt und weniger wichtige Aufgaben um einen Rendering-Zyklus verzögert werden. Dadurch entsteht der dringend benötigte Raum, in dem der Browser die Kontrolle übernimmt und den wichtigen Code auf dem Bildschirm darstellt:

function saveSettings() {
	// Kritische Arbeiten, die für den Benutzer sichtbar sind, ausführen:
	validateForm();
	showSpinner();
	updateUI();

	// Arbeiten, die nicht für den Benutzer sichtbar sind, in eine separate Aufgabe verschieben:
	setTimeout(() => {
		saveToDatabase();
		sendAnalytics();
	}, 0);
}

Im aktuellen Rendering-Zyklus führen wir ein Update im UI durch. Das Ereignis in die Datenbank und die Analytik wird verzögert und asynchron ausgeführt, wodurch die Aufgabe in zwei Teile geteilt wird.

Guter Diener, aber schlechter Herr

Nichts ist umsonst. Diese Optimierungsmethode sieht einfach aus, aber in der Einfachheit steckt oft der Teufel. Es gibt Situationen, in denen die Verzögerung von Code mit setTimeout Komplikationen verursachen kann.

Vorsicht bei der Analytik

Das Verzögern des Trackings von Informationen über einen Link-Klick kann eine analytische Katastrophe verursachen. Wenn der Klick das Laden einer neuen Seite auslöst (wobei kein SPA-Routing gemeint ist), wird die Ausführung von JavaScript angehalten und der mit setTimeout(fn, 0) verzögerte Code nicht aufgerufen. Verzögern Sie nur dann die Messung, wenn Sie wissen, dass der Benutzer auf derselben Seite bleibt.

Anzahl der verzögerten Aufgaben

Aufgaben werden mit setTimeout(fn, 0) lediglich in die Warteschlange zur späteren Bearbeitung gestellt. Wenn Sie viele Aufgaben und Arbeiten verschieben, schaffen Sie sich erneut eine lange Aufgabe in der Zukunft. Darüber hinaus müssen Sie bedenken, dass der Browser auch andere Dinge zu tun hat, als Ihre Timer zu verarbeiten. Verzögern Sie bewusst und vorsichtig. Sie können auch ein wenig mit dem zweiten Parameter des API, der Zeit, spielen und so die Priorität der Aufgaben virtuell beeinflussen.

JavaScript-Frameworks

Alle modernen JavaScript-Frameworks haben eigene Mechanismen zur Bearbeitung von Aufgaben und Interaktionen. Ein unvorsichtiger Aufruf von setTimeout(fn, 0) könnte zu einem Konflikt der Zustände führen. Informieren Sie sich immer, welches der empfohlene Ansatz in Ihrem Framework ist, um Code um einen Rendering-Zyklus zu verschieben. Bei der Optimierung von React sollten Sie beachten, dass useEffect nicht immer asynchron ist.

Das Ganze ist ein „Hack“

Man könnte sagen, dass die Verwendung der Funktion setTimeout() aus Sicht der Optimierung langer Aufgaben ein Hack ist. Immerhin ist es ein API, das dazu dient, eine Aufgabe zu einem späteren Zeitpunkt zu planen und auszuführen.

Es gibt hier keine Verbindung mit der Leistung des Browsers, und dass der Code im nächsten Rendering-Zyklus ausgeführt wird, ist eher ein Nebeneffekt. Die Spezifikation reagiert auf dieses Bedürfnis, und es entstehen neue APIs wie scheduler.postTask() und scheduler.yield(). Die globale Unterstützung in den Browsern fehlt noch. Mit der Nutzung von „graceful degradation“ kann man natürlich mit dem API experimentieren.

setTimeout vs scheduler.yield() Der Unterschied zwischen setTimeout und scheduler.yield(). Eine mit yield aufgeteilte Aufgabe wird priorisierter als andere Aufgaben in der Warteschlange behandelt.

Andere Alternativen zur Verzögerung von Aufgaben

In JavaScript gibt es natürlich mehrere Möglichkeiten, die Ausführung eines Codes zu verzögern. Diese dienen jedoch primär einem anderen Zweck, ähnlich wie setTimeout.

  1. Das API window.requestAnimationFrame() wird oft als Alternative genannt. Es dient jedoch zur Synchronisierung berechneter Stile mit DOM-Elementen. Das heißt, es ist die ideale Methode für animierte Elemente, die mit JavaScript animiert werden. Wenn Sie dieses API zum „Aufteilen von Aufgaben“ verwenden, werden Sie die Reaktionsgeschwindigkeit wahrscheinlich nur verschlechtern.
  2. Das API window.requestIdleCallback() dient zwar der Geschwindigkeitsoptimierung und der Verzögerung von Arbeiten, bis der Browser Ruhe hat. Die Ausführungszeit haben Sie jedoch überhaupt nicht unter Kontrolle, weshalb es für bestimmte Aktionen, wie z.B. Analytik, völlig ungeeignet sein kann.
  3. Das API MessageChannel(), kann ebenfalls genutzt werden, um Code auf den nächsten Rendering-Zyklus zu verschieben. Intern wird es von React verwendet. Es ist jedoch erneut nur ein weiterer Hack.

Fazit

Auch wenn wir wissen, dass setTimeout ein „Hack zur Optimierung von INP“ ist, hat es einen wesentlichen Vorteil. Seine Verwendung ist sehr einfach und schnell.

In puncto Unterstützung ist es derzeit die einzige unfehlbare Lösung, da das Timer-API in JavaScript seit jeher existiert. Wenn Sie jedoch können, wählen Sie modernere APIs wie scheduler.yield() oder andere, langfristig tragbare Lösungen.

Wohin als Nächstes?