Skriptauswertung und lange Aufgaben

Beim Laden von Scripts benötigt der Browser Zeit, um sie vor der Ausführung zu bewerten. Dies kann zu langen Aufgaben führen. Hier erfahren Sie, wie die Scriptauswertung funktioniert und was Sie tun können, damit beim Laden der Seite keine langen Aufgaben ausgeführt werden.

Wenn es um die Optimierung des Messwerts „Interaction to Next Paint“ (INP) geht, wird in den meisten Fällen empfohlen, die Interaktionen selbst zu optimieren. Im Leitfaden zum Optimieren langer Aufgaben werden beispielsweise Techniken wie das Ausgeben mit setTimeout behandelt. Diese Techniken sind vorteilhaft, da sie dem Haupt-Thread etwas Zeit verschaffen, indem lange Aufgaben vermieden werden. Dadurch können Interaktionen und andere Aktivitäten früher ausgeführt werden, als wenn sie auf eine einzige lange Aufgabe warten müssten.

Was ist aber mit den langen Aufgaben, die durch das Laden von Scripts selbst entstehen? Diese Aufgaben können die Nutzerinteraktionen beeinträchtigen und sich während des Ladevorgangs auf den INP einer Seite auswirken. In diesem Leitfaden erfahren Sie, wie Browser Aufgaben verarbeiten, die durch die Scriptauswertung ausgelöst werden. Außerdem wird erläutert, wie Sie die Scriptauswertung unterbrechen können, damit Ihr Hauptthread während des Ladens der Seite besser auf Nutzereingaben reagieren kann.

Was ist die Script-Bewertung?

Wenn Sie eine Anwendung mit viel JavaScript geprofilet haben, haben Sie möglicherweise lange Aufgaben mit dem Label Script auswerten gesehen.

Die Skriptauswertung funktioniert wie im Leistungsprofil der Chrome DevTools dargestellt. Die Arbeit führt zu einer langen Aufgabe beim Starten, die verhindert, dass der Haupt-Thread auf Nutzerinteraktionen reagieren kann.
Die Scriptauswertung funktioniert wie im Leistungsprofil in den Chrome-Entwicklertools dargestellt. In diesem Fall ist die Arbeit ausreichend, um eine lange Aufgabe zu verursachen, die den Hauptthread daran hindert, andere Aufgaben zu übernehmen, einschließlich Aufgaben, die Nutzerinteraktionen auslösen.

Die Script-Bewertung ist ein notwendiger Bestandteil der Ausführung von JavaScript im Browser, da JavaScript Just-in-Time vor der Ausführung kompiliert wird. Wenn ein Script ausgewertet wird, wird es zuerst auf Fehler geprüft. Wenn der Parser keine Fehler findet, wird das Script in Bytecode kompiliert und kann dann ausgeführt werden.

Die Scriptauswertung ist zwar notwendig, kann aber problematisch sein, da Nutzer möglicherweise versuchen, kurz nach dem ersten Rendern mit einer Seite zu interagieren. Nur weil eine Seite gerendert wurde, bedeutet das nicht, dass sie vollständig geladen wurde. Interaktionen, die während des Ladevorgangs stattfinden, können verzögert werden, weil die Seite gerade Scripts auswertet. Es gibt zwar keine Garantie dafür, dass eine Interaktion zu diesem Zeitpunkt stattfinden kann, da das dafür verantwortliche Script möglicherweise noch nicht geladen wurde, aber es kann sein, dass Interaktionen, die von JavaScript abhängen, bereit sind oder dass die Interaktivität überhaupt nicht von JavaScript abhängt.

Die Beziehung zwischen Scripts und den Aufgaben, die sie ausführen

Wie Aufgaben, die für die Scriptauswertung verantwortlich sind, gestartet werden, hängt davon ab, ob das geladene Script mit einem typischen <script>-Element oder mit dem type=module-Element geladen wird. Da Browser dazu neigen, Dinge unterschiedlich zu handhaben, wird kurz darauf eingegangen, wie die wichtigsten Browser-Engines die Scriptauswertung handhaben, wenn sich das Verhalten der Scriptauswertung zwischen ihnen unterscheidet.

Mit dem <script>-Element geladene Scripts

Die Anzahl der Aufgaben, die zur Auswertung von Scripts gesendet werden, steht in der Regel in direktem Zusammenhang mit der Anzahl der <script>-Elemente auf einer Seite. Jedes <script>-Element startet eine Aufgabe zur Auswertung des angeforderten Scripts, damit es geparst, kompiliert und ausgeführt werden kann. Das gilt für Chromium-basierte Browser, Safari und Firefox.

Warum ist das relevant? Angenommen, Sie verwenden einen Bundler, um Ihre Produktionsscripts zu verwalten, und haben ihn so konfiguriert, dass alles, was für die Ausführung Ihrer Seite erforderlich ist, in einem einzigen Script gebündelt wird. Wenn dies auf Ihre Website zutrifft, wird für die Auswertung dieses Scripts nur eine einzige Aufgabe gesendet. Ist das etwas Schlechtes? Nicht unbedingt, es sei denn, das Script ist riesig.

Sie können die Scriptauswertung aufteilen, indem Sie das Laden großer JavaScript-Chunks vermeiden und mithilfe zusätzlicher <script>-Elemente mehr einzelne, kleinere Scripts laden.

Sie sollten beim Seitenaufbau immer versuchen, so wenig JavaScript wie möglich zu laden. Wenn Sie Ihre Scripts jedoch aufteilen, haben Sie anstelle einer großen Aufgabe, die den Hauptthread blockieren kann, eine größere Anzahl kleinerer Aufgaben, die den Hauptthread gar nicht oder zumindest weniger blockieren.

Mehrere Aufgaben, die die Scriptauswertung umfassen, wie im Leistungsprofil von Chrome DevTools dargestellt. Da mehrere kleinere Scripts anstelle weniger größerer Scripts geladen werden, ist die Wahrscheinlichkeit geringer, dass Aufgaben zu langen Aufgaben werden. So kann der Hauptthread schneller auf Nutzereingaben reagieren.
Mehrere Tasks wurden gestartet, um Scripts aufgrund mehrerer <script>-Elemente im HTML-Code der Seite zu bewerten. Das ist besser als das Senden eines großen Script-Bundles an die Nutzer, was mit größerer Wahrscheinlichkeit den Haupt-Thread blockiert.

Das Aufteilen von Aufgaben für die Scriptauswertung ähnelt dem Ausgeben von Werten während Ereignis-Callbacks, die während einer Interaktion ausgeführt werden. Bei der Scriptauswertung wird das von Ihnen geladene JavaScript jedoch durch den Yielding-Mechanismus in mehrere kleinere Scripts aufgeteilt, anstatt in eine kleinere Anzahl größerer Scripts, die mit größerer Wahrscheinlichkeit den Hauptthread blockieren.

Scripts, die mit dem Element <script> und dem Attribut type=module geladen werden

Mit dem type=module-Attribut auf dem <script>-Element können ES-Module jetzt nativ im Browser geladen werden. Dieser Ansatz für das Script-Laden bietet einige Vorteile für Entwickler, da der Code nicht für die Produktionsnutzung transformiert werden muss – insbesondere in Kombination mit Importkarten. Beim Laden von Scripts auf diese Weise werden jedoch Aufgaben geplant, die sich von Browser zu Browser unterscheiden.

Chromium-basierte Browser

In Browsern wie Chrome oder in denen, die von Chrome abgeleitet sind, werden beim Laden von ES-Modulen mit dem Attribut type=module andere Arten von Aufgaben ausgeführt als bei Verwendung von type=module. Beispielsweise wird für jedes Modulscript eine Aufgabe ausgeführt, die die Aktivität Modul kompilieren umfasst.

Die Modulkompilierung umfasst mehrere Aufgaben, wie in den Chrome DevTools dargestellt.
Verhalten beim Laden von Modulen in Chromium-basierten Browsern. Jedes Modulscript ruft Compile module auf, um den Inhalt vor der Auswertung zu kompilieren.

Sobald die Module kompiliert wurden, wird durch jeden Code, der anschließend in ihnen ausgeführt wird, eine Aktivität mit der Bezeichnung Modul bewerten gestartet.

Just-in-time-Bewertung eines Moduls, wie im Bereich „Leistung“ der Chrome-Entwicklertools dargestellt.
Wenn Code in einem Modul ausgeführt wird, wird dieses Modul Just-in-Time ausgewertet.

Die Folge ist, dass die Kompilierungsschritte bei Verwendung von ES-Modulen zumindest in Chrome und ähnlichen Browsern aufgeteilt werden. Das ist ein klarer Vorteil bei der Verwaltung langer Aufgaben. Die daraus resultierende Modulbewertung bedeutet jedoch, dass Sie unvermeidliche Kosten verursachen. Sie sollten zwar versuchen, so wenig JavaScript wie möglich zu verwenden, aber die Verwendung von ES-Modulen bietet unabhängig vom Browser folgende Vorteile:

  • Der gesamte Modulcode wird automatisch im strengen Modus ausgeführt. Dadurch sind potenzielle Optimierungen durch JavaScript-Engines möglich, die in einem nicht strengen Kontext nicht möglich wären.
  • Scripts, die mit type=module geladen werden, werden standardmäßig als verzögert behandelt. Mit dem async-Attribut in Scripts, die mit type=module geladen werden, lässt sich dieses Verhalten ändern.

Safari und Firefox

Wenn Module in Safari und Firefox geladen werden, wird jedes in einer separaten Aufgabe ausgewertet. Das bedeutet, dass Sie theoretisch ein einzelnes Modul auf oberster Ebene mit nur statischen import-Anweisungen in andere Module laden könnten. Für jedes geladene Modul wird eine separate Netzwerkanfrage und -aufgabe zur Auswertung ausgelöst.

Mit dynamischer import() geladene Scripts

Dynamische import() ist eine weitere Methode zum Laden von Scripts. Im Gegensatz zu statischen import-Anweisungen, die sich oben in einem ES-Modul befinden müssen, kann ein dynamischer import()-Aufruf an einer beliebigen Stelle in einem Script erscheinen, um einen JavaScript-Codeblock bei Bedarf zu laden. Diese Methode wird als Code-Splitting bezeichnet.

Dynamische import() hat zwei Vorteile bei der Verbesserung der INP:

  1. Module, deren Laden verschoben wird, reduzieren die Hauptthread-Konflikte beim Starten, da zu diesem Zeitpunkt weniger JavaScript geladen wird. Dadurch wird der Hauptthread freigegeben, sodass er schneller auf Nutzerinteraktionen reagieren kann.
  2. Bei dynamischen import()-Aufrufen wird bei jedem Aufruf die Kompilierung und Auswertung der einzelnen Module effektiv in eine eigene Aufgabe unterteilt. Natürlich löst ein dynamisches import(), das ein sehr großes Modul lädt, eine ziemlich große Script-Bewertungsaufgabe aus. Das kann die Fähigkeit des Hauptthreads beeinträchtigen, auf Nutzereingaben zu reagieren, wenn die Interaktion gleichzeitig mit dem dynamischen import()-Aufruf erfolgt. Es ist daher weiterhin sehr wichtig, so wenig JavaScript wie möglich zu laden.

Dynamische import()-Aufrufe verhalten sich in allen gängigen Browser-Engines ähnlich: Die Anzahl der resultierenden Script-Bewertungsaufgaben entspricht der Anzahl der dynamisch importierten Module.

In einem Webworker geladene Scripts

Webworker sind ein spezieller JavaScript-Anwendungsfall. Webworker werden im Hauptthread registriert und der Code im Worker wird dann in einem eigenen Thread ausgeführt. Das ist sehr vorteilhaft, da der Code, der den Webworker registriert, im Hauptthread ausgeführt wird, der Code im Webworker jedoch nicht. Dadurch wird die Überlastung des Hauptthreads reduziert und der Hauptthread kann schneller auf Nutzerinteraktionen reagieren.

Neben der Reduzierung der Arbeit im Hauptthread können Webworker selbst externe Scripts laden, die im Worker-Kontext verwendet werden sollen. Dies ist entweder über importScripts oder über statische import-Anweisungen in Browsern möglich, die Modul-Worker unterstützen. Das Ergebnis ist, dass jedes Skript, das von einem Webworker angefordert wird, außerhalb des Haupt-Threads ausgewertet wird.

Vor- und Nachteile sowie Überlegungen

Wenn Sie Ihre Scripts in separate, kleinere Dateien aufteilen, können Sie lange Aufgaben besser begrenzen, als wenn Sie weniger, aber viel größere Dateien laden. Bei der Entscheidung, wie Sie Scripts aufteilen, sollten Sie jedoch einige Dinge beachten.

Komprimierungseffizienz

Komprimierung ist ein Faktor, der beim Aufteilen von Scripts eine Rolle spielt. Bei kleineren Scripts ist die Komprimierung etwas weniger effizient. Größere Scripts profitieren viel stärker von der Komprimierung. Eine höhere Komprimierungseffizienz trägt zwar dazu bei, die Ladezeiten für Scripts so gering wie möglich zu halten, aber es ist ein wenig schwierig, Scripts in genügend kleinere Teile zu zerlegen, um eine bessere Interaktivität beim Start zu ermöglichen.

Bundler sind ideale Tools zur Verwaltung der Ausgabegröße für die Scripts, von denen Ihre Website abhängt:

  • Bei webpack kann das SplitChunksPlugin-Plug-in helfen. In der SplitChunksPlugin-Dokumentation finden Sie Optionen, mit denen Sie die Asset-Größe verwalten können.
  • Bei anderen Bundlern wie Rollup und esbuild können Sie die Größe von Scriptdateien mithilfe dynamischer import()-Aufrufe in Ihrem Code verwalten. Diese Bundler und auch Webpack trennen das dynamisch importierte Asset automatisch in eine eigene Datei, um größere anfängliche Bundle-Größen zu vermeiden.

Cache-Entwertung

Die Cache-Invalidierung spielt eine große Rolle bei der Geschwindigkeit, mit der eine Seite bei wiederholten Besuchen geladen wird. Wenn Sie große, monolithische Script-Bundles bereitstellen, sind Sie beim Browser-Caching im Nachteil. Das liegt daran, dass das gesamte Bundle ungültig wird und noch einmal heruntergeladen werden muss, wenn Sie Ihren Code aktualisieren – entweder durch Aktualisieren von Paketen oder durch das Bereitstellen von Fehlerkorrekturen.

Wenn Sie Ihre Scripts aufteilen, verteilen Sie die Scriptauswertung nicht nur auf kleinere Aufgaben, sondern erhöhen auch die Wahrscheinlichkeit, dass wiederkehrende Besucher mehr Scripts aus dem Browsercache statt aus dem Netzwerk abrufen. Das führt zu einer insgesamt schnelleren Seitenladezeit.

Verschachtelte Module und Ladeleistung

Wenn Sie ES-Module in der Produktion bereitstellen und mit dem Attribut type=module laden, müssen Sie wissen, wie sich das Nesting von Modulen auf die Startzeit auswirken kann. Bei der Modulverschachtelung wird ein ES-Modul statisch in ein anderes ES-Modul importiert, das wiederum statisch in ein anderes ES-Modul importiert wird:

// a.js
import {b} from './b.js';

// b.js
import {c} from './c.js';

Wenn Ihre ES-Module nicht gebündelt sind, führt der Code oben zu einer Netzwerkanfragekette: Wenn a.js von einem <script>-Element angefordert wird, wird eine weitere Netzwerkanfrage für b.js gesendet, was dann eine weitere Anfrage für c.js nach sich zieht. Eine Möglichkeit, dies zu vermeiden, besteht darin, einen Bundler zu verwenden. Achten Sie jedoch darauf, den Bundler so zu konfigurieren, dass Scripts aufgeteilt werden, um die Scriptauswertung zu verteilen.

Wenn Sie keinen Bundler verwenden möchten, können Sie verschachtelte Modulaufrufe auch mithilfe des modulepreload-Ressourcenhinweises umgehen. Dabei werden ES-Module vorab geladen, um Netzwerkanfrageketten zu vermeiden.

Fazit

Die Bewertung von Scripts im Browser zu optimieren, ist zweifellos eine schwierige Aufgabe. Der Ansatz hängt von den Anforderungen und Einschränkungen Ihrer Website ab. Wenn Sie Scripts jedoch aufteilen, verteilen Sie die Arbeit der Scriptauswertung auf zahlreiche kleinere Aufgaben und ermöglichen dem Hauptthread so, Nutzerinteraktionen effizienter zu verarbeiten, anstatt den Hauptthread zu blockieren.

Hier sind einige Möglichkeiten, wie Sie große Script-Bewertungsaufgaben aufteilen können:

  • Wenn Sie Scripts mit dem <script>-Element ohne das type=module-Attribut laden, sollten Sie keine sehr großen Scripts laden, da diese ressourcenintensive Script-Bewertungsaufgaben auslösen, die den Hauptthread blockieren. Verteilen Sie Ihre Scripts auf mehrere <script>-Elemente, um diese Arbeit aufzuteilen.
  • Wenn Sie das Attribut type=module verwenden, um ES-Module nativ im Browser zu laden, werden für jedes einzelne Modulscript separate Aufgaben zur Auswertung gestartet.
  • Mit dynamischen import()-Aufrufen können Sie die Größe Ihrer ursprünglichen Bundles reduzieren. Das funktioniert auch in Bundlern, da diese jedes dynamisch importierte Modul als „Trennpunkt“ behandeln. Dadurch wird für jedes dynamisch importierte Modul ein separates Script generiert.
  • Berücksichtigen Sie dabei auch Kompromisse wie Komprimierungseffizienz und Cache-Invalidierung. Größere Scripts lassen sich besser komprimieren, erfordern aber mit größerer Wahrscheinlichkeit eine aufwendigere Scriptauswertung bei weniger Aufgaben und führen zur Invalidierung des Browser-Caches, was zu einer insgesamt geringeren Cache-Effizienz führt.
  • Wenn Sie ES-Module nativ ohne Bündelung verwenden, können Sie mit dem Ressourcenhinweis modulepreload das Laden der Module beim Start optimieren.
  • Wie immer gilt: Senden Sie so wenig JavaScript wie möglich.

Es ist in der Tat ein Balanceakt. Wenn Sie Scripts jedoch aufteilen und die anfänglichen Nutzlasten mit dynamischen import() reduzieren, können Sie eine bessere Startleistung erzielen und Nutzerinteraktionen in dieser wichtigen Startphase besser berücksichtigen. So sollten Sie bei diesem Messwert besser abschneiden und die Nutzerfreundlichkeit verbessern.