Beim Laden von Skripten benötigt der Browser Zeit, um sie vor der Ausführung auszuwerten. Dies kann zu langen Aufgaben führen. Hier erfahren Sie, wie die Skriptauswertung funktioniert und was Sie tun können, um zu verhindern, dass sie während des Seitenaufbaus lange Aufgaben verursacht.
Wenn es um die Optimierung von Interaction to Next Paint (INP) geht, beziehen sich die meisten Empfehlungen auf die Optimierung von Interaktionen selbst. Im Leitfaden zum Optimieren langer Aufgaben werden beispielsweise Techniken wie das Yielding mit setTimeout behandelt. Diese Techniken sind nützlich, da sie dem Hauptthread etwas Spielraum geben, indem sie lange Aufgaben vermeiden. Dadurch können Interaktionen und andere Aktivitäten schneller ausgeführt werden, als wenn sie auf eine einzelne lange Aufgabe warten müssten.
Was ist aber mit den langen Aufgaben, die durch das Laden von Skripten entstehen? Diese Aufgaben können Nutzerinteraktionen beeinträchtigen und sich auf den INP einer Seite während des Ladevorgangs auswirken. In diesem Guide wird erläutert, wie Browser Aufgaben verarbeiten, die durch die Skriptauswertung ausgelöst werden. Außerdem wird untersucht, wie Sie die Skriptauswertung aufteilen können, damit Ihr Hauptthread während des Ladens der Seite besser auf Nutzereingaben reagieren kann.
Was ist die Skriptbewertung?
Wenn Sie ein Profil für eine Anwendung erstellt haben, die viel JavaScript enthält, haben Sie möglicherweise lange Aufgaben gesehen, bei denen Evaluate Script als Ursache angegeben ist.
Die Script-Auswertung ist ein notwendiger Bestandteil der Ausführung von JavaScript im Browser, da JavaScript erst kurz vor der Ausführung kompiliert wird. Wenn ein Skript ausgewertet wird, wird es zuerst auf Fehler analysiert. Wenn der Parser keine Fehler findet, wird das Script in Bytecode kompiliert und kann dann ausgeführt werden.
Die Skriptauswertung ist zwar notwendig, kann aber problematisch sein, da Nutzer möglicherweise kurz nach dem ersten Rendern versuchen, mit einer Seite zu interagieren. Nur weil eine Seite gerendert wurde, bedeutet das jedoch nicht, dass sie auch geladen wurde. Interaktionen, die während des Ladevorgangs stattfinden, können sich verzögern, da die Seite mit der Auswertung von Skripts beschäftigt ist. Es gibt zwar keine Garantie dafür, dass zu diesem Zeitpunkt eine Interaktion stattfinden kann, da ein dafür verantwortliches Script möglicherweise noch nicht geladen wurde, aber es kann Interaktionen geben, die von JavaScript abhängig sind und bereit sind, oder die Interaktivität hängt überhaupt nicht von JavaScript ab.
Beziehung zwischen Scripts und den Aufgaben, mit denen sie bewertet werden
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 als Modul mit dem type=module geladen wird. Da Browser dazu neigen, Dinge unterschiedlich zu handhaben, wird darauf eingegangen, wie die wichtigsten Browser-Engines die Skriptauswertung handhaben, wenn sich das Verhalten bei der Skriptauswertung zwischen ihnen unterscheidet.
Mit dem <script>-Element geladene Skripts
Die Anzahl der Aufgaben, die zum Bewerten 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, um das angeforderte Skript zu bewerten, 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 alles, was Ihre Seite zum Ausführen benötigt, in einem einzigen Skript gebündelt wird. Wenn dies auf Ihre Website zutrifft, wird wahrscheinlich nur eine Aufgabe zur Überprüfung dieses Skripts gesendet. Ist das etwas Schlechtes? Nicht unbedingt, es sei denn, das Skript ist sehr lang.
Sie können die Auswertung von Scripts aufteilen, indem Sie das Laden großer JavaScript-Blöcke vermeiden und stattdessen einzelne, kleinere Scripts mit zusätzlichen <script>-Elementen laden.
Sie sollten immer versuchen, beim Seitenaufbau so wenig JavaScript wie möglich zu laden. Wenn Sie Ihre Skripts aufteilen, haben Sie anstelle einer großen Aufgabe, die den Hauptthread blockieren kann, eine größere Anzahl kleinerer Aufgaben, die den Hauptthread überhaupt nicht oder zumindest weniger blockieren als zuvor.
<script>-Elemente vorhanden sind. Das ist besser, als Nutzern ein großes Script-Bundle zu senden, da dies den Hauptthread eher blockiert.
Das Aufteilen von Aufgaben für die Script-Auswertung ist in etwa vergleichbar mit dem Yielding während Ereignis-Callbacks, die während einer Interaktion ausgeführt werden. Bei der Skriptauswertung wird das geladene JavaScript jedoch in mehrere kleinere Skripts aufgeteilt, anstatt in eine kleinere Anzahl größerer Skripts, die den Hauptthread eher blockieren.
Skripts, die mit dem Element <script> und dem Attribut type=module geladen werden
Mit dem Attribut type=module für das Element <script> können ES-Module jetzt nativ im Browser geladen werden. Diese Methode zum Laden von Skripts bietet einige Vorteile für Entwickler, z. B. dass kein Code für die Produktionsumgebung transformiert werden muss, insbesondere in Kombination mit Import Maps. Wenn Sie Skripts auf diese Weise laden, werden jedoch Aufgaben geplant, die sich von Browser zu Browser unterscheiden.
Auf Chromium basierende Browser
In Browsern wie Chrome oder Browsern, die auf Chrome basieren, werden beim Laden von ES-Modulen mit dem Attribut type=module andere Arten von Aufgaben ausgeführt als normalerweise, wenn type=module nicht verwendet wird. So wird beispielsweise für jedes Modulskript eine Aufgabe ausgeführt, die die Aktivität mit dem Label Modul kompilieren umfasst.
Sobald die Module kompiliert wurden, wird durch jeden Code, der anschließend in ihnen ausgeführt wird, eine Aktivität mit dem Label Modul auswerten ausgelöst.
In Chrome und ähnlichen Browsern werden die Kompilierungsschritte bei Verwendung von ES-Modulen aufgeteilt. Das ist ein klarer Vorteil bei der Verwaltung langer Aufgaben. Die resultierende Modulbewertung führt jedoch zu unvermeidlichen Kosten. 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 Strict Mode ausgeführt. Dadurch können JavaScript-Engines potenzielle Optimierungen vornehmen, die in einem nicht strengen Kontext nicht möglich wären.
- Skripts, die mit
type=modulegeladen werden, werden standardmäßig so behandelt, als wären sie aufgeschoben. Sie können dieses Verhalten ändern, indem Sie das Attributasyncfür Skripts verwenden, die mittype=modulegeladen werden.
Safari und Firefox
Wenn Module in Safari und Firefox geladen werden, wird jedes von ihnen in einer separaten Aufgabe ausgewertet. Theoretisch könnten Sie also ein einzelnes Modul der obersten Ebene laden, das nur statische import-Anweisungen für andere Module enthält. Für jedes geladene Modul wird eine separate Netzwerkanfrage und Aufgabe zur Auswertung gestellt.
Mit dynamischem import() geladene Skripts
Dynamisches import() ist eine weitere Methode zum Laden von Scripts. Im Gegensatz zu statischen import-Anweisungen, die am Anfang eines ES-Moduls stehen müssen, kann ein dynamischer import()-Aufruf an einer beliebigen Stelle in einem Skript erfolgen, um einen JavaScript-Chunk bei Bedarf zu laden. Dieses Verfahren wird als Code-Splitting bezeichnet.
Dynamisches import() hat zwei Vorteile, wenn es darum geht, den INP zu verbessern:
- Module, deren Laden auf später verschoben wird, verringern die Belastung des Hauptthreads beim Start, da dann weniger JavaScript geladen wird. Dadurch wird der Hauptthread entlastet, sodass er schneller auf Nutzerinteraktionen reagieren kann.
- Bei dynamischen
import()-Aufrufen werden Kompilierung und Auswertung jedes Moduls effektiv in separate Aufgaben unterteilt. Wenn ein dynamischesimport()ein sehr großes Modul lädt, wird natürlich eine ziemlich umfangreiche Aufgabe zur Skriptauswertung gestartet. Das kann die Fähigkeit des Hauptthreads beeinträchtigen, auf Nutzereingaben zu reagieren, wenn die Interaktion gleichzeitig mit dem dynamischenimport()-Aufruf erfolgt. Es ist daher weiterhin sehr wichtig, dass Sie so wenig JavaScript wie möglich laden.
Dynamische import()-Aufrufe verhalten sich in allen wichtigen Browser-Engines ähnlich: Die resultierenden Script-Evaluierungsaufgaben entsprechen der Anzahl der dynamisch importierten Module.
In einem Webworker geladene Skripts
Web Workers sind ein spezieller JavaScript-Anwendungsfall. Web-Worker werden im Hauptthread registriert und der Code im Worker wird dann in einem eigenen Thread ausgeführt. Das ist sehr vorteilhaft, da der Code, mit dem der Web-Worker registriert wird, zwar im Hauptthread ausgeführt wird, der Code im Web-Worker jedoch nicht. Dadurch wird die Überlastung des Hauptthreads reduziert und er kann besser auf Nutzerinteraktionen reagieren.
Neben der Reduzierung der Arbeit im Hauptthread können Web-Worker selbst externe Skripts laden, die im Worker-Kontext verwendet werden sollen. Dies kann entweder über importScripts oder über statische import-Anweisungen in Browsern erfolgen, die Modul-Worker unterstützen. Das Ergebnis ist, dass jedes von einem Webworker angeforderte Skript außerhalb des Haupt-Threads ausgewertet wird.
Vor- und Nachteile und wichtige Aspekte
Wenn Sie Ihre Skripts in separate, kleinere Dateien aufteilen, können Sie lange Aufgaben begrenzen, anstatt weniger, aber viel größere Dateien zu laden. Bei der Entscheidung, wie Sie Skripts aufteilen, sollten Sie jedoch einige Dinge berücksichtigen.
Komprimierungseffizienz
Die Komprimierung spielt eine Rolle, wenn es darum geht, Skripts aufzuteilen. Bei kleineren Skripts ist die Komprimierung etwas weniger effizient. Bei größeren Scripts ist der Nutzen der Komprimierung viel größer. Eine höhere Komprimierungseffizienz trägt dazu bei, die Ladezeiten für Skripts so gering wie möglich zu halten. Es ist jedoch wichtig, Skripts in ausreichend kleine Teile aufzuteilen, um eine bessere Interaktivität beim Start zu ermöglichen.
Bundler sind ideale Tools, um die Ausgabegröße der Skripts zu verwalten, von denen Ihre Website abhängt:
- Für webpack kann das
SplitChunksPlugin-Plug-in hilfreich sein. Informationen zu Optionen, die Sie festlegen können, um die Asset-Größen zu verwalten, finden Sie in derSplitChunksPlugin-Dokumentation. - Bei anderen Bundlern wie Rollup und esbuild können Sie die Größe von Scriptdateien mit dynamischen
import()-Aufrufen in Ihrem Code verwalten. Diese Bundler sowie webpack teilen das dynamisch importierte Asset automatisch in eine eigene Datei auf, um größere anfängliche Bundle-Größen zu vermeiden.
Cache-Entwertung
Die Cache-Invalidierung spielt eine große Rolle dabei, wie schnell eine Seite bei wiederholten Besuchen geladen wird. Wenn Sie große, monolithische Script-Bundles ausliefern, haben Sie einen Nachteil beim Browser-Caching. Das liegt daran, dass das gesamte Bundle ungültig wird und noch einmal heruntergeladen werden muss, wenn Sie Ihren selbst erhobenen Code aktualisieren, z. B. durch Aktualisieren von Paketen oder Bereitstellen von Fehlerkorrekturen.
Wenn Sie Ihre Skripts aufteilen, verteilen Sie die Auswertung von Skripts nicht nur auf kleinere Aufgaben, sondern erhöhen auch die Wahrscheinlichkeit, dass wiederkehrende Besucher mehr Skripts aus dem Browsercache anstatt aus dem Netzwerk abrufen. Das führt zu einem insgesamt schnelleren Seitenaufbau.
Geschachtelte Module und Ladeleistung
Wenn Sie ES-Module in der Produktion versenden und sie mit dem Attribut type=module laden, müssen Sie wissen, wie sich das Verschachteln 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 obige Code zu einer Kette von Netzwerkanfragen: Wenn a.js von einem <script>-Element angefordert wird, wird eine weitere Netzwerkanfrage für b.js gesendet, die dann eine weitere Anfrage für c.js beinhaltet. Eine Möglichkeit, dies zu vermeiden, ist die Verwendung eines Bundlers. Achten Sie jedoch darauf, dass Sie Ihren Bundler so konfigurieren, dass Skripts aufgeteilt werden, um die Auswertung von Skripts zu verteilen.
Wenn Sie keinen Bundler verwenden möchten, können Sie verschachtelte Modulaufrufe auch mit dem Ressourcenhinweis modulepreload umgehen. Dadurch werden ES-Module vorab geladen, um Ketten von Netzwerkanfragen zu vermeiden.
Fazit
Die Optimierung der Auswertung von Scripts 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 zahlreiche kleinere Aufgaben. So kann der Hauptthread Nutzerinteraktionen effizienter verarbeiten, anstatt blockiert zu werden.
Zusammenfassend finden Sie hier einige Möglichkeiten, um große Skriptbewertungsaufgaben aufzuteilen:
- Wenn Sie Skripts mit dem
<script>-Element ohne dastype=module-Attribut laden, sollten Sie keine sehr großen Skripts laden, da dadurch ressourcenintensive Aufgaben zur Skriptauswertung ausgelöst werden, die den Hauptthread blockieren. Verteilen Sie Ihre Skripts auf mehrere<script>-Elemente, um die Arbeit aufzuteilen. - Wenn Sie das Attribut
type=moduleverwenden, um ES-Module nativ im Browser zu laden, werden für jedes separate Modulskript einzelne Aufgaben zur Auswertung gestartet. - Reduzieren Sie die Größe Ihrer ersten Bundles, indem Sie dynamische
import()-Aufrufe verwenden. Das funktioniert auch in Bundlern, da diese jedes dynamisch importierte Modul als „Split Point“ behandeln. Dadurch wird für jedes dynamisch importierte Modul ein separates Skript generiert. - Berücksichtigen Sie dabei Kompromisse wie Komprimierungseffizienz und Cache-Invalidierung. Größere Skripts lassen sich besser komprimieren, erfordern aber wahrscheinlich mehr Aufwand für die Skriptauswertung in weniger Aufgaben und führen zu einer Invalidierung des Browser-Cache, was insgesamt zu einer geringeren Caching-Effizienz führt.
- Wenn Sie ES-Module nativ ohne Bündelung verwenden, können Sie den Ressourcenhinweis
modulepreloadverwenden, um das Laden während des Starts zu optimieren. - Wie immer gilt: Senden Sie so wenig JavaScript wie möglich.
Das ist natürlich ein Balanceakt. Wenn Sie aber Skripts aufteilen und die anfänglichen Nutzlasten mit dynamischen import() reduzieren, können Sie die Startleistung verbessern und Nutzerinteraktionen während dieser wichtigen Startphase besser berücksichtigen. Das sollte Ihnen helfen, beim INP-Messwert besser abzuschneiden und so die Nutzerfreundlichkeit zu verbessern.