Lange Aufgaben optimieren

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.

Screenshot einer Aufgabe im Leistungsprofil der Chrome-Entwicklertools. Die Aufgabe befindet sich oben in einem Stapel und enthält einen Klickereignis-Handler, einen Funktionsaufruf und weitere Elemente darunter. Die Aufgabe umfasst auch einige Renderingarbeiten auf der rechten Seite.
Eine Aufgabe, die von einem click-Event-Handler gestartet wurde und im Leistungsprofiler der Chrome-Entwicklertools zu sehen ist.

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.

Eine lange Aufgabe im Leistungsprofiler der Chrome-Entwicklertools. Der blockierende Teil der Aufgabe (mehr als 50 Millisekunden) ist durch rote diagonale Streifen gekennzeichnet.
Eine lange Aufgabe, die im Leistungsprofiler von Chrome angezeigt wird. Lange Aufgaben werden durch ein rotes Dreieck in der Ecke der Aufgabe gekennzeichnet, wobei der blockierende Teil der Aufgabe mit einem Muster diagonaler roter Streifen ausgefüllt ist.

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.

Eine einzelne lange Aufgabe im Vergleich zu einer einzelnen Aufgabe, die in kürzere Aufgaben aufgeteilt wird. Die lange Aufgabe besteht aus einem großen Rechteck und die aufgeteilte Aufgabe besteht aus fünf kleineren Feldern, deren Länge der langen Aufgabe entspricht.
Eine Visualisierung einer einzelnen langen Aufgabe im Vergleich zur selben Aufgabe in fünf kürzere Aufgaben.

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.

Das Aufteilen einer Aufgabe kann die Interaktion der Nutzenden erleichtern. Oben wird die Ausführung eines Event-Handlers durch eine lange Aufgabe blockiert, bis die Aufgabe abgeschlossen ist. Unten kann der Event-Handler dank der aufgeteilten Aufgabe früher ausgeführt werden als sonst.
Wenn Aufgaben zu lang sind, kann der Browser nicht schnell genug auf Interaktionen reagieren. Durch Aufteilen von Aufgaben können diese Interaktionen schneller ablaufen.

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();
}
Die Funktion „saveSettings“ im Leistungsprofiler von Chrome. Während die Funktion der obersten Ebene fünf andere Funktionen aufruft, findet die gesamte Arbeit in einer langen Aufgabe statt, die den Hauptthread blockiert.
Eine einzelne Funktion saveSettings(), die fünf Funktionen aufruft. Die Arbeit wird als Teil einer langen monolithischen Aufgabe ausgeführt.

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.

Dieselbe Funktion „saveSettings“ im Leistungsprofiler von Chrome, jetzt mit Resultate.
    Die Aufgabe wird jetzt in fünf separate Aufgaben unterteilt, eine für jede Funktion.
Die Funktion saveSettings() führt jetzt ihre untergeordneten Funktionen als separate Aufgaben aus.

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.

Unterstützte Browser

  • 94
  • 94
  • x

Quelle

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 kein priority 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.

Die Funktion „saveSettings“ wird im Leistung-Profiler von Chrome angezeigt, allerdings mit postTask. „postTask“ teilt jede Funktion auf, die „saveSettings“ ausgeführt wird, und priorisiert sie so, dass eine Nutzerinteraktion ohne Blockierung ausgeführt werden kann.
Wenn saveSettings() ausgeführt wird, plant die Funktion die einzelnen Funktionsaufrufe mit postTask(). Die kritischen nutzerseitigen Arbeiten werden mit hoher Priorität geplant, während dem Nutzer nicht bekannte Arbeit im Hintergrund ausgeführt wird. Dadurch können Nutzerinteraktionen schneller ausgeführt werden, da die Arbeit sowohl aufgeteilt als auch entsprechend priorisiert wird.

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.

Drei Diagramme, die Aufgaben ohne ein Ergebnis, mit einem Ergebnis sowie mit einem Ergebnis und einer Fortsetzung zeigen. Ohne ein Ergebnis zu liefern, sind lange Aufgaben angesetzt. Beim Output gibt es mehr Aufgaben, die kürzer sind, aber durch andere Aufgaben unterbrochen werden können. Bei Erzeugung und Fortsetzung bleibt die Ausführungsreihenfolge der kürzeren Aufgaben erhalten.
Wenn Sie scheduler.yield() verwenden, wird die Aufgabenausführung auch nach dem Ertragspartner dort fortgesetzt, wo sie aufgehört hat.

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.

Ertrag aus Eingabe mit isInputPending()

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() und scheduler.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.