Gängige Tipps zur Beschleunigung von JavaScript-Anwendungen sind oft „den Hauptthread nicht blockieren“ und „Lange Aufgaben aufteilen“. Auf dieser Seite wird erläutert, was diese Ratschläge bedeutet und warum die Optimierung von Aufgaben in JavaScript wichtig ist.
Was ist eine Aufgabe?
Eine Aufgabe ist jede eigenständige Arbeit, die der Browser erledigt. Dazu gehören das Rendern, das Parsen von HTML- und CSS-Code, das Ausführen des von Ihnen geschriebenen JavaScript-Codes und andere Dinge, über die Sie möglicherweise keine direkte Kontrolle haben. Das JavaScript Ihrer Seiten ist eine wichtige Quelle für Browseraufgaben.
Aufgaben wirken sich auf verschiedene Weise auf die Leistung aus. Wenn ein Browser beispielsweise beim Start eine JavaScript-Datei herunterlädt, stellt er Aufgaben zum Parsen und kompilieren dieses JavaScript in die Warteschlange, damit es ausgeführt werden kann. Später im Seitenlebenszyklus beginnen andere Aufgaben, wenn Ihr JavaScript funktioniert. Beispielsweise beginnen Interaktionen über Event-Handler, JavaScript-gesteuerte Animationen und Hintergrundaktivitäten wie die Analysesammlung. All dies geschieht im Hauptthread, mit Ausnahme von Web Workern und ähnlichen APIs.
Was ist der Hauptthread?
Im Hauptthread werden die meisten Aufgaben im Browser und fast der gesamte von Ihnen geschriebene JavaScript-Code ausgeführt.
Der Hauptthread kann jeweils nur eine Aufgabe verarbeiten. Jede Aufgabe, die länger als 50 Millisekunden dauert, zählt als lange Aufgabe. Wenn der Nutzer versucht, während einer langen Aufgabe oder einer Rendering-Aktualisierung mit der Seite zu interagieren, muss der Browser auf die Verarbeitung dieser Interaktion warten, was zu Latenz führt.
Um dies zu verhindern, sollten Sie lange Aufgaben in kleinere Aufgaben aufteilen, die jeweils weniger Zeit in Anspruch nehmen. Dies wird als Aufteilen langer Aufgaben bezeichnet.
Das Aufteilen von Aufgaben gibt dem Browser mehr Möglichkeiten, auf Aufgaben mit höherer Priorität zu reagieren, einschließlich Nutzerinteraktionen zwischen anderen Aufgaben. Dadurch können Interaktionen viel schneller ausgeführt werden, wenn ein Nutzer andernfalls eine Verzögerung bemerkt hätte, während der Browser auf eine lange Aufgabe wartet.
Strategien für das Aufgabenmanagement
JavaScript behandelt jede Funktion als einzelne Aufgabe, da es ein Modell zur Ausführung bis zum Abschluss der Aufgabenausführung verwendet. Das bedeutet, dass eine Funktion, die mehrere andere Funktionen aufruft, wie im folgenden Beispiel, ausgeführt werden muss, bis alle aufgerufenen Funktionen abgeschlossen sind. Dadurch wird der Browser verlangsamt:
function saveSettings () { //This is a long task.
validateForm();
showSpinner();
saveToDatabase();
updateUI();
sendAnalytics();
}
Wenn Ihr Code Funktionen enthält, die mehrere Methoden aufrufen, teilen Sie ihn in mehrere Funktionen auf. Dies gibt dem Browser nicht nur mehr Möglichkeiten, auf Interaktionen zu reagieren, sondern erleichtert auch das Lesen, Verwalten und Schreiben von Tests für Ihren Code. In den folgenden Abschnitten werden einige Strategien zur Aufteilung langer Funktionen und Priorisierung der Aufgaben beschrieben, aus denen sie bestehen.
Codeausführung manuell aufschieben
Sie können die Ausführung einiger Aufgaben verschieben, indem Sie die entsprechende Funktion an setTimeout()
übergeben. Dies funktioniert auch, wenn Sie ein Zeitlimit von 0
angeben.
function saveSettings () {
// Do critical work that is user-visible:
validateForm();
showSpinner();
updateUI();
// Defer work that isn't user-visible to a separate task:
setTimeout(() => {
saveToDatabase();
sendAnalytics();
}, 0);
}
Dies funktioniert am besten für eine Reihe von Funktionen, die in einer bestimmten Reihenfolge ausgeführt werden müssen. unterschiedlich organisierter Code
benötigt einen anderen Ansatz. Das nächste Beispiel ist eine Funktion, die eine große Datenmenge in einer Schleife verarbeitet. Je größer das Dataset ist, desto länger dauert dies und es gibt nicht unbedingt eine gute Stelle in der Schleife, um ein setTimeout()
einzufügen:
function processData () {
for (const item of largeDataArray) {
// Process the individual item here.
}
}
Glücklicherweise gibt es einige andere APIs, mit denen Sie die Codeausführung auf eine spätere Aufgabe zurückstellen können. Wir empfehlen die Verwendung von postMessage()
für schnellere Zeitüberschreitungen.
Sie können die Arbeit auch mit requestIdleCallback()
aufteilen. Dabei werden Aufgaben mit der niedrigsten Priorität und nur während der Inaktivität des Browsers geplant. Das bedeutet, dass mit requestIdleCallback()
geplante Aufgaben möglicherweise nie ausgeführt werden, wenn der Hauptthread besonders ausgelastet ist.
Mit async
/await
Ertragsgruppen erstellen
Damit wichtige für Nutzer sichtbare Aufgaben vor Aufgaben mit niedrigerer Priorität ausgeführt werden, wechseln Sie zum Hauptthread. Unterbrechen Sie dazu kurz die Aufgabenwarteschlange. So erhält der Browser die Möglichkeit, wichtigere Aufgaben auszuführen.
Die einfachste Möglichkeit hierfür ist ein Promise
, das mit einem Aufruf von setTimeout()
aufgelöst wird:
function yieldToMain () {
return new Promise(resolve => {
setTimeout(resolve, 0);
});
}
In der saveSettings()
-Funktion können Sie nach jedem Schritt an den Hauptthread liefern, wenn Sie die yieldToMain()
-Funktion nach jedem Funktionsaufruf await
ausführen. Dadurch wird Ihre lange Aufgabe effektiv in mehrere Aufgaben aufgeteilt:
async function saveSettings () {
// Create an array of functions to run:
const tasks = [
validateForm,
showSpinner,
saveToDatabase,
updateUI,
sendAnalytics
]
// Loop over the tasks:
while (tasks.length > 0) {
// Shift the first task off the tasks array:
const task = tasks.shift();
// Run the task:
task();
// Yield to the main thread:
await yieldToMain();
}
}
Wichtig: Sie müssen nicht nach jedem Funktionsaufruf Ergebnisse liefern. Wenn Sie beispielsweise zwei Funktionen ausführen, die wichtige Aktualisierungen der Benutzeroberfläche zur Folge haben, sollten Sie zwischen den Funktionen wahrscheinlich keine Ergebnisse liefern. Wenn möglich, lassen Sie diese Arbeit zuerst ausführen und dann sollten Sie überlegen, zwischen Funktionen zu liefern, die im Hintergrund ausgeführt werden, oder weniger wichtige Aufgaben, die der Nutzer nicht sieht.
Eine dedizierte Planer-API
Die bisher erwähnten APIs können Ihnen helfen, Aufgaben aufzuteilen, haben aber einen erheblichen Nachteil: Wenn Sie den Hauptthread durch Verschieben von Code zur Ausführung in einer späteren Aufgabe bereitstellen, wird dieser Code am Ende der Aufgabenwarteschlange hinzugefügt.
Wenn Sie den gesamten Code auf Ihrer Seite verwalten, können Sie Ihren eigenen Planer erstellen, um Aufgaben zu priorisieren. Da Skripte von Drittanbietern Ihren Planer jedoch nicht verwenden, können Sie in diesem Fall die Arbeit nicht wirklich priorisieren. Sie können sie nur aufteilen oder nach Nutzerinteraktionen liefern.
Die Scheduler API bietet die Funktion postTask()
, die eine präzisere Planung von Aufgaben ermöglicht und dem Browser hilft, Aufgaben so zu priorisieren, dass dem Hauptthread Aufgaben mit niedriger Priorität zugutekommen. postTask()
verwendet Promis und akzeptiert eine priority
-Einstellung.
Die postTask()
API hat drei verfügbare Prioritäten:
'background'
für Aufgaben mit der niedrigsten Priorität.'user-visible'
für Aufgaben mit mittlerer Priorität. Dies ist die Standardeinstellung, wenn keinpriority
festgelegt ist.'user-blocking'
für kritische Aufgaben, die mit hoher Priorität ausgeführt werden müssen.
Im folgenden Beispielcode wird die postTask()
API verwendet, um drei Aufgaben mit der höchstmöglichen Priorität auszuführen. Die verbleibenden beiden Aufgaben werden mit der niedrigsten Priorität ausgeführt:
function saveSettings () {
// Validate the form at high priority
scheduler.postTask(validateForm, {priority: 'user-blocking'});
// Show the spinner at high priority:
scheduler.postTask(showSpinner, {priority: 'user-blocking'});
// Update the database in the background:
scheduler.postTask(saveToDatabase, {priority: 'background'});
// Update the user interface at high priority:
scheduler.postTask(updateUI, {priority: 'user-blocking'});
// Send analytics data in the background:
scheduler.postTask(sendAnalytics, {priority: 'background'});
};
Hier wird die Priorität von Aufgaben so geplant, dass browserpriorisierte Aufgaben, wie Nutzerinteraktionen, übernommen werden können.
Sie können auch verschiedene TaskController
-Objekte instanziieren, die Prioritäten zwischen Aufgaben haben. Dazu gehört auch die Möglichkeit, Prioritäten für verschiedene TaskController
-Instanzen nach Bedarf zu ändern.
Integrierter Ertrag bei Fortsetzung mithilfe der kommenden scheduler.yield()
API
Wichtig: Eine ausführlichere Erläuterung von scheduler.yield()
finden Sie in der Erklärung des Ursprungstests (seit Abschluss) und der Erklärung.
Eine vorgeschlagene Ergänzung der Scheduler API ist scheduler.yield()
, eine API, die speziell für die Ausgabe des Hauptthreads im Browser entwickelt wurde. Ihre Verwendung entspricht der Funktion yieldToMain()
, die weiter oben auf dieser Seite gezeigt wurde:
async function saveSettings () {
// Create an array of functions to run:
const tasks = [
validateForm,
showSpinner,
saveToDatabase,
updateUI,
sendAnalytics
]
// Loop over the tasks:
while (tasks.length > 0) {
// Shift the first task off the tasks array:
const task = tasks.shift();
// Run the task:
task();
// Yield to the main thread with the scheduler
// API's own yielding mechanism:
await scheduler.yield();
}
}
Dieser Code ist weitgehend vertraut, aber anstelle von yieldToMain()
wird await scheduler.yield()
verwendet.
Der Vorteil von scheduler.yield()
ist eine Fortsetzung. Wenn Sie also in der Mitte einer Reihe von Aufgaben arbeiten, werden die anderen geplanten Aufgaben in derselben Reihenfolge nach dem Ertragspunkt fortgesetzt. Dadurch wird verhindert, dass Drittanbieterskripts die Kontrolle über die Reihenfolge übernehmen, in der Ihr Code ausgeführt wird.
Die Verwendung von scheduler.postTask()
mit priority: 'user-blocking'
weist aufgrund der hohen Priorität user-blocking
auch eine hohe Wahrscheinlichkeit einer Fortsetzung auf. Sie können diese also als Alternative verwenden, bis scheduler.yield()
allgemein verfügbar ist.
Mit setTimeout()
(oder scheduler.postTask()
mit priority: 'user-visible'
oder ohne explizites priority
) wird die Aufgabe am Ende der Warteschlange geplant. So können andere ausstehende Aufgaben vor der Fortsetzung ausgeführt werden.
isInputPending()
nicht verwenden
Unterstützte Browser
- 87
- 87
- x
- x
Mit der isInputPending()
API kann geprüft werden, ob ein Nutzer versucht hat, mit einer Seite zu interagieren, und den Vorgang nur erfolgt, wenn eine Eingabe aussteht.
Dadurch kann JavaScript fortgesetzt werden, wenn keine Eingaben ausstehen, anstatt die Ausgabe am Ende der Aufgabenwarteschlange vorzunehmen. Dies kann zu beeindruckenden Leistungsverbesserungen bei Websites führen, die andernfalls nicht an den Hauptthread liefern könnten, wie im Intent to Ship beschrieben.
Seit der Einführung dieser API hat sich unser Verständnis von Ertrag verbessert, insbesondere nach der Einführung von INP. Wir empfehlen, diese API nicht mehr zu verwenden. Stattdessen sollten Sie die Ausgabe unabhängig davon, ob die Eingabe aussteht oder nicht zurückgegeben wird, empfehlen. Für diese Änderung bei den Empfehlungen gibt es mehrere Gründe:
- Manchmal gibt die API fälschlicherweise
false
zurück, wenn ein Nutzer mit einer Interaktion interagiert hat. - Input ist nicht der einzige Fall, in dem Aufgaben etwas liefern sollten. Animationen und andere regelmäßige Aktualisierungen der Benutzeroberfläche können für die Bereitstellung einer responsiven Webseite genauso wichtig sein.
- Seitdem wurden umfassendere Ertragsgruppen wie
scheduler.postTask()
undscheduler.yield()
eingeführt, um Bedenken auszuräumen.
Fazit
Das Verwalten von Aufgaben ist eine Herausforderung, aber wenn Sie es damit tun, kann Ihre Seite schneller auf Nutzerinteraktionen reagieren. Je nach Anwendungsfall gibt es eine Vielzahl von Techniken zum Verwalten und Priorisieren von Aufgaben. Noch einmal: Das sind die wichtigsten Aspekte, die Sie beim Verwalten von Aufgaben berücksichtigen sollten:
- Gibt den Hauptthread für kritische Aufgaben für die Nutzer zurück.
- Du kannst mit
scheduler.yield()
experimentieren. - Aufgaben mit
postTask()
priorisieren. - Und schließlich: Arbeiten Sie so wenig wie möglich an Ihren Funktionen.
Mit einem oder mehreren dieser Tools sollten Sie in der Lage sein, die Arbeit in Ihrer Anwendung so zu strukturieren, dass die Anforderungen der Nutzer priorisiert werden und gleichzeitig weniger wichtige Arbeiten ausgeführt werden. Dies verbessert die Reaktionsfähigkeit und die Nutzerfreundlichkeit.
Wir danken Philip Walton für die technische Prüfung dieses Dokuments.
Thumbnail-Bild von Unsplash mit freundlicher Genehmigung von Amirali Mirhashemian.