Skriptauswertung und lange Aufgaben

Beim Laden von Skripts dauert es einige Zeit, bis der Browser sie vor der Ausführung bewertet, was zu langen Aufgaben führen kann. Hier erfahren Sie, wie die Skriptauswertung funktioniert und was Sie tun können, um lange Aufgaben beim Seitenaufbau zu vermeiden.

Bei der Optimierung von Interaction to Next Paint (INP) sollten Sie die Interaktionen selbst optimieren. Beispielsweise werden im Leitfaden zum Optimieren langer Aufgaben Techniken wie das Ausgeben von Ergebnissen mit setTimeout und andere behandelt. Diese Techniken sind vorteilhaft, da sie dem Hauptthread etwas Raum geben, da sie lange Aufgaben vermeiden und dadurch mehr Möglichkeiten für Interaktionen und andere Aktivitäten bieten können, anstatt auf eine einzige lange Aufgabe warten zu müssen.

Aber was ist mit den langen Aufgaben, die durch das Laden der Skripts selbst entstehen? Diese Aufgaben können Nutzerinteraktionen beeinträchtigen und den INP einer Seite beim Laden beeinflussen. In diesem Leitfaden erfahren Sie, wie Browser Aufgaben handhaben, die durch die Skriptauswertung ausgelöst werden. Außerdem erfahren Sie, wie Sie die Skriptbewertung so aufteilen können, dass Ihr Hauptthread besser auf Nutzereingaben reagiert, während die Seite geladen wird.

Was ist Skriptbewertung?

Wenn Sie ein Profil für eine Anwendung erstellt haben, die viel JavaScript versendet, sind Ihnen möglicherweise lange Aufgaben aufgefallen, bei denen der Täter die Bezeichnung Skript auswerten enthält.

Die Skriptauswertung funktioniert wie im Leistungsprofiler der Chrome-Entwicklertools dargestellt. Der Arbeitsaufwand verursacht eine lange Aufgabe beim Start, wodurch der Hauptthread nicht mehr auf Nutzerinteraktionen reagieren kann.
Die Skriptauswertung funktioniert wie im Leistungsprofiler in den Chrome-Entwicklertools dargestellt. In diesem Fall reicht die Arbeit aus, um eine lange Aufgabe zu verursachen, die den Hauptthread daran hindert, andere Aufgaben zu übernehmen, einschließlich Aufgaben, die Nutzerinteraktionen fördern.

Die Skriptauswertung ist bei der Ausführung von JavaScript im Browser erforderlich, da JavaScript genau vor der Ausführung kompiliert wird. Bei der Auswertung eines Skripts wird es zuerst auf Fehler überprüft. Wenn der Parser keine Fehler findet, wird das Skript in Bytecode kompiliert und kann mit der Ausführung fortgesetzt werden.

Die Skriptauswertung kann bei Bedarf jedoch problematisch sein, da Nutzer versuchen könnten, kurz nach dem ersten Rendern mit einer Seite zu interagieren. Wenn eine Seite jedoch gerendert wurde, bedeutet das noch lange nicht, dass sie vollständig geladen wurde. Interaktionen, die während des Ladevorgangs stattfinden, können sich verzögern, da die Seite damit beschäftigt ist, Scripts auszuwerten. Obwohl es keine Garantie dafür gibt, dass zu diesem Zeitpunkt eine Interaktion stattfinden kann, kann es sein, dass ein für sie verantwortliches Skript noch nicht geladen wurde. Es könnten jedoch Interaktionen vorhanden sein, die von JavaScript abhängig sind und bereit sind, oder die Interaktivität hängt überhaupt nicht von JavaScript ab.

Die Beziehung zwischen Skripten und den Aufgaben, die sie auswerten

Wie Aufgaben, die für die Skriptbewertung verantwortlich sind, gestartet werden, hängt davon ab, ob das zu ladende Skript mit einem typischen <script>-Element geladen wird oder ob das Skript ein Modul ist, das mit dem type=module geladen wurde. Da Browser die Tendenz haben, Dinge unterschiedlich zu behandeln, wird darauf hingewiesen, wie die großen Browser-Engines die Skriptauswertung handhaben.

Mit dem Element <script> geladene Skripts

Die Anzahl der Aufgaben, die zum Auswerten von Skripts weitergeleitet werden, steht in der Regel in direktem Zusammenhang mit der Anzahl der <script>-Elemente auf einer Seite. Jedes <script>-Element startet eine Aufgabe, um das angeforderte Skript auszuwerten, 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 Produktionsskripts zu verwalten, und haben ihn so konfiguriert, dass er alles, was für die Ausführung Ihrer Seite erforderlich ist, in einem einzigen Skript bündelt. Wenn dies für Ihre Website gilt, können Sie davon ausgehen, dass eine einzelne Aufgabe zur Auswertung des Skripts ausgeführt wird. Ist das etwas Schlechtes? Nicht unbedingt – es sei denn, das Skript ist groß.

Sie können die Skriptauswertung aufteilen, indem Sie das Laden großer JavaScript-Blöcke vermeiden und mehr einzelne, kleinere Skripts mit zusätzlichen <script>-Elementen laden.

Sie sollten immer versuchen, so wenig JavaScript wie möglich während des Seitenaufbaus zu laden. Durch das Aufteilen Ihrer Skripts wird sichergestellt, dass Sie anstelle einer großen Aufgabe, die den Hauptthread blockieren könnte, eine größere Anzahl kleinerer Aufgaben haben, die den Hauptthread überhaupt nicht blockieren – oder zumindest weniger als die, mit der Sie begonnen haben.

Mehrere Aufgaben mit Skriptauswertung, wie im Performance-Profiler der Chrome-Entwicklertools visualisiert. Da mehrere kleinere Skripts anstelle von weniger größeren Skripts geladen werden, ist die Wahrscheinlichkeit geringer, dass aus Aufgaben zu langen Aufgaben werden. Dadurch kann der Hauptthread schneller auf Nutzereingaben reagieren.
Aufgrund mehrerer <script>-Elemente im HTML-Code der Seite werden mehrere Aufgaben zur Auswertung von Skripts erzeugt. Dies ist besser, als ein großes Skript-Bundle an Nutzer zu senden, da sonst der Hauptthread mit größerer Wahrscheinlichkeit blockiert wird.

Die Aufteilung von Aufgaben für die Skriptbewertung ähnelt in etwa der Auswirkung während Ereignis-Callbacks, die während einer Interaktion ausgeführt werden. Bei der Skriptauswertung wird das von Ihnen geladene JavaScript jedoch in mehrere kleinere Skripts aufgeteilt, anstatt eine kleinere Anzahl größerer Skripts zu haben, als es wahrscheinlicher ist, dass der Hauptthread blockiert wird.

Mit dem Element <script> und dem Attribut type=module geladene Skripts

Es ist jetzt möglich, ES-Module nativ im Browser zu laden. Dazu wird das type=module-Attribut auf dem <script>-Element verwendet. Dieser Ansatz zum Laden von Skripts bringt einige Vorteile für Entwickler mit sich. So muss z. B. kein Code für die Produktion transformiert werden, insbesondere wenn er in Kombination mit Importkarten verwendet wird. Wenn Skripts auf diese Weise geladen werden, werden jedoch Aufgaben geplant, die sich von Browser zu Browser unterscheiden.

Chromium-basierte Browser

In Browsern wie Chrome – oder solchen, die davon abgeleitet sind – führt das Laden von ES-Modulen mit dem Attribut type=module zu anderen Aufgaben, als dies normalerweise der Fall wäre, wenn Sie type=module nicht verwenden. Beispielsweise wird für jedes Modulskript eine Aufgabe ausgeführt, die eine Aktivität mit dem Label Modul kompilieren umfasst.

Die Modulkompilierung umfasst mehrere Aufgaben, wie in den Chrome-Entwicklertools dargestellt.
Ladeverhalten des Moduls in Chromium-basierten Browsern. Jedes Modulskript erzeugt einen Compile-Modul-Aufruf, um den Inhalt vor der Auswertung zu kompilieren.

Sobald die Module kompiliert sind, startet jeder Code, der anschließend darin ausgeführt wird, die Aktivität Bewerten Modul.

Just-in-time-Auswertung eines Moduls, wie im Leistungsbereich der Chrome-Entwicklertools dargestellt.
Wenn in einem Modul Code ausgeführt wird, wird dieses Modul bedarfsgerecht ausgewertet.

Zumindest in Chrome und verwandten Browsern bedeutet das, dass die Kompilierungsschritte bei der Verwendung von ES-Modulen unterbrochen werden. Das ist ein klarer Vorteil, was die Verwaltung langer Aufgaben angeht. Die daraus resultierende Modulbewertung bedeutet jedoch immer noch, dass Ihnen unvermeidliche Kosten entstehen. Sie sollten zwar versuchen, so wenig JavaScript wie möglich zu senden, aber die Verwendung von ES-Modulen bietet unabhängig vom Browser die folgenden Vorteile:

  • Der gesamte Modulcode wird automatisch im strikten Modus ausgeführt. Dadurch sind potenzielle Optimierungen durch JavaScript-Engines möglich, die andernfalls in einem nicht strikten Kontext nicht durchgeführt werden könnten.
  • Mit type=module geladene Skripts werden standardmäßig so behandelt, als wären sie zurückgestellt. Sie können dieses Verhalten mithilfe des Attributs async bei Skripts ändern, die mit type=module geladen werden.

Safari und Firefox

Beim Laden von Modulen in Safari und Firefox wird jedes Modul in einer separaten Aufgabe ausgewertet. Das bedeutet, dass Sie theoretisch ein einzelnes Modul der obersten Ebene, das nur aus statischen import-Anweisungen besteht, in andere Module laden können. Für jedes geladene Modul wird eine separate Netzwerkanfrage und eine separate Aufgabe zur Auswertung ausgeführt.

Skripts mit dynamischem import() geladen

Dynamisches import() ist eine weitere Methode zum Laden von Skripts. 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 im Script erscheinen, um bei Bedarf einen JavaScript-Abschnitt zu laden. Diese Technik wird als Codeaufteilung bezeichnet.

Dynamische import() bieten bei der Verbesserung von INP zwei Vorteile:

  1. Module, die erst später geladen werden, reduzieren den Hauptthreadkonflikt während des Starts, indem sie die zu diesem Zeitpunkt geladene JavaScript-Menge reduzieren. Dadurch wird der Hauptthread freigegeben und kann besser auf Nutzerinteraktionen reagieren.
  2. Bei dynamischen import()-Aufrufen wird bei jedem Aufruf die Kompilierung und Auswertung der einzelnen Module in eine eigene Aufgabe unterteilt. Natürlich löst ein dynamisches import(), das ein sehr großes Modul lädt, eine ziemlich umfangreiche Skriptbewertung aus. Dies kann die Fähigkeit des Hauptthreads beeinträchtigen, auf Nutzereingaben zu reagieren, wenn die Interaktion zur selben Zeit wie der dynamische import()-Aufruf stattfindet. Daher ist es dennoch sehr wichtig, dass so wenig JavaScript wie möglich geladen wird.

Dynamische import()-Aufrufe verhalten sich in allen gängigen Browser-Engines ähnlich: Die resultierenden Skriptbewertungsaufgaben entsprechen der Anzahl der Module, die dynamisch importiert werden.

In einem Web Worker geladene Skripts

Web Worker sind ein spezieller Anwendungsfall für JavaScript. Web Worker werden im Hauptthread registriert und der Code innerhalb des Workers wird dann in einem eigenen Thread ausgeführt. Dies hat den Vorteil, dass der Code, der den Web Worker registriert, im Hauptthread ausgeführt wird, der Code im Web Worker jedoch nicht. Dies reduziert die Überlastung des Hauptthreads und trägt dazu bei, dass der Hauptthread besser auf Nutzerinteraktionen reagiert.

Web Worker reduzieren nicht nur den Aufwand des Hauptthreads, sondern können selbst externe Skripts laden, um sie im Worker-Kontext zu laden. Dies geschieht entweder über importScripts- oder statische import-Anweisungen in Browsern, die Modul-Worker unterstützen. Als Ergebnis wird jedes von einem Web Worker angeforderte Skript aus dem Hauptthread ausgewertet.

Vor- und Nachteile

Wenn Sie Ihre Skripts in separate, kleinere Dateien aufteilen, können Sie lange Aufgaben begrenzen, anstatt weniger, viel größere Dateien zu laden. Bei der Entscheidung, wie Sie Skripts aufteilen, sind einige Dinge zu berücksichtigen.

Kompressionseffizienz

Die Komprimierung ist ein Faktor bei der Trennung von Skripts. Wenn Skripts kleiner sind, wird die Komprimierung etwas weniger effizient. Größere Skripts profitieren viel besser von der Komprimierung. Eine höhere Komprimierungseffizienz trägt zwar dazu bei, die Ladezeiten von Skripts so gering wie möglich zu halten, aber es ist ein Balanceakt, um sicherzustellen, dass Skripts in kleinere Einheiten aufgeteilt werden, um eine bessere Interaktivität beim Start zu ermöglichen.

Bundler sind ideale Tools, um die Ausgabegröße für die Skripts zu verwalten, von denen Ihre Website abhängig ist:

  • Bei Problemen mit Webpack kann das SplitChunksPlugin-Plug-in Abhilfe schaffen. In der SplitChunksPlugin-Dokumentation finden Sie Optionen zum Verwalten der Asset-Größen.
  • Bei anderen Bundlern wie Rollup und esbuild können Sie die Größe der Skriptdateien verwalten, indem Sie dynamische import()-Aufrufe in Ihrem Code verwenden. Diese Bundler und Webpacks unterteilen das dynamisch importierte Asset automatisch in eine eigene Datei, um größere Bundle-Größen zu vermeiden.

Cache-Entwertungen

Die Cache-Entwertung spielt eine wichtige Rolle dabei, wie schnell eine Seite bei wiederholten Besuchen geladen wird. Wenn Sie große, monolithische Script-Bundles versenden, sind Sie beim Browser-Caching im Nachteil. Das liegt daran, dass das gesamte Paket ungültig wird und neu heruntergeladen werden muss, wenn Sie Ihren eigenen Code aktualisieren – entweder durch das Aktualisieren von Paketen oder durch Versandfehlerkorrekturen.

Indem Sie Ihre Skripts aufteilen, aufteilen Sie die Skriptbewertung nicht nur auf kleinere Aufgaben, sondern erhöhen auch die Wahrscheinlichkeit, dass wiederkehrende Besucher mehr Skripts aus dem Browser-Cache statt aus dem Netzwerk abrufen. Dadurch wird die Seite insgesamt schneller geladen.

Verschachtelte Module und Ladeleistung

Wenn Sie ES-Module in der Produktion versenden und mit dem Attribut type=module laden, sollten Sie sich darüber im Klaren sein, wie sich die Modulverschachtelung auf die Startzeit auswirken kann. Die Modulverschachtelung bezieht sich auf den Vorgang, wenn ein ES-Modul ein anderes ES-Modul statisch importiert, das ein anderes ES-Modul statisch importiert:

// 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 vorherige Code zu einer Netzwerkanfragekette: Wenn a.js von einem <script>-Element angefordert wird, wird für b.js eine weitere Netzwerkanfrage gesendet, die dann eine weitere Anfrage für c.js beinhaltet. Eine Möglichkeit, dies zu vermeiden, ist die Verwendung eines Bundler. Sie sollten Ihren Bundler jedoch so konfigurieren, dass Skripts aufgeteilt werden, um die Skriptauswertung zu verteilen.

Wenn Sie keinen Bundler verwenden möchten, können Sie verschachtelte Modulaufrufe auch umgehen, indem Sie den Ressourcenhinweis modulepreload verwenden. Dadurch werden ES-Module im Voraus geladen, um Netzwerkanfrageketten zu vermeiden.

Fazit

Die Optimierung der Auswertung von Skripts im Browser ist zweifellos eine schwierige Aufgabe. Der Ansatz hängt von den Anforderungen und Einschränkungen Ihrer Website ab. Wenn Sie Skripts jedoch aufteilen, verteilen Sie die Arbeit der Skriptauswertung auf viele kleinere Aufgaben, sodass der Hauptthread Nutzerinteraktionen effizienter verarbeiten kann, anstatt den Hauptthread zu blockieren.

Hier sind einige Dinge, die Sie tun können, um große Aufgaben zur Skriptbewertung aufzuteilen:

  • Wenn du Skripts mit dem Element <script> ohne das Attribut type=module lädst, solltest du keine sehr großen Skripts laden, da diese sonst ressourcenintensive Skriptauswertungen auslösen, die den Hauptthread blockieren. Verteilen Sie Ihre Skripts auf mehr <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 Modulskript einzelne Aufgaben zur Auswertung gestartet.
  • Reduzieren Sie die Größe Ihrer ersten Sets durch dynamische import()-Aufrufe. Dies funktioniert auch in Bundler, da Bundler jedes dynamisch importierte Modul als Splitpoint behandeln, sodass für jedes dynamisch importierte Modul ein separates Skript generiert wird.
  • Achten Sie darauf, Kompromisse wie Komprimierungseffizienz und Cache-Entwertung abzuwägen. Größere Skripts werden besser komprimiert, erfordern jedoch mit größerer Wahrscheinlichkeit eine teurere Skriptbewertung mit weniger Aufgaben und führen zu einer Entwertung des Browser-Cache, was insgesamt zu einer geringeren Caching-Effizienz führt.
  • Wenn Sie ES-Module nativ ohne Bündelung verwenden, verwenden Sie den Ressourcenhinweis modulepreload, um das Laden der Module während des Starts zu optimieren.
  • Stellen Sie wie immer möglichst wenig JavaScript bereit.

Das ist mit Sicherheit ein Balanceakt. Aber wenn Sie Skripts aufbrechen und die anfängliche Nutzlast mit dem dynamischen import() reduzieren, können Sie eine bessere Startleistung erzielen und Nutzerinteraktionen während dieser entscheidenden Startphase besser berücksichtigen. Dies sollte Ihnen helfen, den INP-Messwert zu verbessern und die Nutzererfahrung zu verbessern.

Hero-Image von Unsplash von Markus Spiske