JavaScript für Codeaufteilung

Das Laden großer JavaScript-Ressourcen wirkt sich erheblich auf die Seitengeschwindigkeit aus. Wenn Sie Ihr JavaScript in kleinere Blöcke aufteilen und nur das herunterladen, was für die Funktion einer Seite beim Start erforderlich ist, kann die Reaktionsfähigkeit beim Laden Ihrer Seite erheblich verbessert werden. Dies wiederum kann die INP-Messwerte (Interaction to Next Paint) Ihrer Seite verbessern.

Wenn eine Seite große JavaScript-Dateien herunterlädt, parst und kompiliert, kann es vorkommen, dass sie für einen bestimmten Zeitraum nicht reagiert. Die Seitenelemente sind sichtbar, da sie Teil des ursprünglichen HTML-Codes einer Seite sind und durch CSS formatiert werden. Das JavaScript, das für diese interaktiven Elemente erforderlich ist, sowie andere Skripts, die von der Seite geladen werden, müssen jedoch möglicherweise geparst und ausgeführt werden, damit sie funktionieren. Das Ergebnis ist, dass der Nutzer das Gefühl haben kann, dass die Interaktion erheblich verzögert oder sogar ganz unterbrochen wurde.

Das liegt oft daran, dass der Hauptthread blockiert ist, da JavaScript im Hauptthread geparst und kompiliert wird. Wenn dieser Vorgang zu lange dauert, reagieren interaktive Seitenelemente möglicherweise nicht schnell genug auf Nutzereingaben. Eine Möglichkeit, dieses Problem zu beheben, besteht darin, nur das JavaScript zu laden, das für die Funktion der Seite erforderlich ist. Anderes JavaScript kann später über eine Technik namens „Code-Splitting“ geladen werden. In diesem Modul geht es um die zweite dieser beiden Techniken.

JavaScript-Parsing und -Ausführung beim Start durch Codeaufteilung reduzieren

Lighthouse gibt eine Warnung aus, wenn die JavaScript-Ausführung länger als 2 Sekunden dauert, und schlägt fehl, wenn sie länger als 3,5 Sekunden dauert. Das Parsen und Ausführen von zu viel JavaScript kann zu jedem Zeitpunkt im Lebenszyklus der Seite ein Problem darstellen, da es das Input Delay einer Interaktion erhöhen kann, wenn der Zeitpunkt, zu dem der Nutzer mit der Seite interagiert, mit dem Zeitpunkt zusammenfällt, zu dem die Hauptthread-Aufgaben zum Verarbeiten und Ausführen von JavaScript ausgeführt werden.

Darüber hinaus ist eine übermäßige JavaScript-Ausführung und ‑Analyse besonders problematisch beim ersten Laden der Seite, da Nutzer in dieser Phase des Seitenlebenszyklus sehr wahrscheinlich mit der Seite interagieren. INP

Der Lighthouse-Audit, in dem die Zeit für die Ausführung jeder JavaScript-Datei angegeben wird, die von Ihrer Seite angefordert wird, kann Ihnen dabei helfen, genau die Skripts zu identifizieren, die für Code-Splitting infrage kommen. Mit dem Tool zur Codeabdeckung in den Chrome-Entwicklertools können Sie dann genau ermitteln, welche Teile des JavaScript einer Seite während des Seitenaufbaus nicht verwendet werden.

Die Codeaufteilung ist eine nützliche Technik, mit der die anfänglichen JavaScript-Nutzlasten einer Seite reduziert werden können. Damit können Sie ein JavaScript-Bundle in zwei Teile aufteilen:

  • Das JavaScript wird beim Laden der Seite benötigt und kann daher nicht zu einem anderen Zeitpunkt geladen werden.
  • Verbleibender JavaScript-Code, der zu einem späteren Zeitpunkt geladen werden kann, meistens dann, wenn der Nutzer mit einem bestimmten interaktiven Element auf der Seite interagiert.

Code-Splitting kann mit der dynamischen import()-Syntax erfolgen. Mit dieser Syntax wird im Gegensatz zu <script>-Elementen, bei denen beim Start eine bestimmte JavaScript-Ressource angefordert wird, eine JavaScript-Ressource erst später im Lebenszyklus der Seite angefordert.

document.querySelectorAll('#myForm input').addEventListener('blur', async () => {
  // Get the form validation named export from the module through destructuring:
  const { validateForm } = await import('/validate-form.mjs');

  // Validate the form:
  validateForm();
}, { once: true });

Im vorherigen JavaScript-Snippet wird das validate-form.mjs-Modul nur dann heruntergeladen, geparst und ausgeführt, wenn ein Nutzer den Fokus von einem der <input>-Felder eines Formulars entfernt. In diesem Fall wird die JavaScript-Ressource, die für die Validierungslogik des Formulars verantwortlich ist, nur dann in die Seite eingebunden, wenn sie höchstwahrscheinlich tatsächlich verwendet wird.

JavaScript-Bundler wie webpack, Parcel, Rollup und esbuild können so konfiguriert werden, dass JavaScript-Bundles in kleinere Chunks aufgeteilt werden, sobald sie in Ihrem Quellcode auf einen dynamischen import()-Aufruf stoßen. Die meisten dieser Tools führen diese Optimierung automatisch durch. Bei esbuild müssen Sie sie jedoch explizit aktivieren.

Hilfreiche Hinweise zum Code-Splitting

Die Codeaufteilung ist eine effektive Methode, um die Belastung des Hauptthreads beim ersten Laden der Seite zu reduzieren. Wenn Sie Ihren JavaScript-Quellcode auf Möglichkeiten zur Codeaufteilung prüfen möchten, sollten Sie jedoch einige Dinge beachten.

Sofern möglich, einen Bundler verwenden

Entwickler verwenden während des Entwicklungsprozesses häufig JavaScript-Module. Das ist eine hervorragende Verbesserung für Entwickler, die die Lesbarkeit und Wartbarkeit des Codes verbessert. Es gibt jedoch einige suboptimale Leistungsmerkmale, die beim Bereitstellen von JavaScript-Modulen in der Produktion auftreten können.

Am wichtigsten ist, dass Sie einen Bundler verwenden, um Ihren Quellcode zu verarbeiten und zu optimieren, einschließlich der Module, die Sie aufteilen möchten. Bundler sind nicht nur sehr effektiv bei der Optimierung von JavaScript-Quellcode, sondern auch beim Ausgleich von Leistungsaspekten wie der Bundle-Größe im Verhältnis zum Komprimierungsverhältnis. Die Komprimierungseffektivität steigt mit der Bundle-Größe. Bundler versuchen jedoch auch, dafür zu sorgen, dass Bundles nicht so groß sind, dass durch die Skriptauswertung lange Aufgaben entstehen.

Bundler vermeiden auch das Problem, eine große Anzahl von nicht gebündelten Modulen über das Netzwerk zu übertragen. Architekturen, die JavaScript-Module verwenden, haben in der Regel große, komplexe Modulbäume. Wenn Modulbäume nicht gebündelt werden, stellt jedes Modul eine separate HTTP-Anfrage dar. Die Interaktivität in Ihrer Web-App kann sich verzögern, wenn Sie Module nicht bündeln. Es ist zwar möglich, den Ressourcenhinweis <link rel="modulepreload"> zu verwenden, um große Modulbäume so früh wie möglich zu laden, aber JavaScript-Bundles sind aus Sicht der Ladeleistung immer noch vorzuziehen.

Streaming-Kompilierung nicht versehentlich deaktivieren

Die V8 JavaScript-Engine von Chromium bietet eine Reihe von Optimierungen, die dafür sorgen, dass Ihr JavaScript-Produktionscode so effizient wie möglich geladen wird. Eine dieser Optimierungen wird als Streaming-Kompilierung bezeichnet. Dabei werden, wie beim inkrementellen Parsen von HTML, das an den Browser gestreamt wird, gestreamte JavaScript-Chunks kompiliert, sobald sie aus dem Netzwerk eintreffen.

Es gibt mehrere Möglichkeiten, dafür zu sorgen, dass die Streaming-Kompilierung für Ihre Webanwendung in Chromium erfolgt:

  • Produktionscode so umwandeln, dass keine JavaScript-Module verwendet werden Bundler können Ihren JavaScript-Quellcode basierend auf einem Kompilierungsziel transformieren. Das Ziel ist oft spezifisch für eine bestimmte Umgebung. V8 wendet die Streaming-Kompilierung auf jeden JavaScript-Code an, in dem keine Module verwendet werden. Sie können Ihren Bundler so konfigurieren, dass Ihr JavaScript-Modulcode in eine Syntax umgewandelt wird, in der keine JavaScript-Module und ihre Funktionen verwendet werden.
  • Wenn Sie JavaScript-Module in der Produktion verwenden möchten, verwenden Sie die .mjs-Erweiterung. Unabhängig davon, ob in Ihrem Produktions-JavaScript Module verwendet werden, gibt es keinen speziellen Inhaltstyp für JavaScript mit Modulen im Vergleich zu JavaScript ohne Module. Bei V8 deaktivieren Sie die Streaming-Kompilierung effektiv, wenn Sie JavaScript-Module in der Produktion mit der Erweiterung .js ausliefern. Wenn Sie die Erweiterung .mjs für JavaScript-Module verwenden, kann V8 dafür sorgen, dass die Streaming-Kompilierung für modulbasierten JavaScript-Code nicht unterbrochen wird.

Lassen Sie sich dadurch nicht davon abhalten, Code-Splitting zu verwenden. Durch Code-Splitting lässt sich die anfängliche JavaScript-Nutzlast für Nutzer effektiv reduzieren. Wenn Sie jedoch einen Bundler verwenden und wissen, wie Sie das Streaming-Kompilierungsverhalten von V8 beibehalten können, können Sie dafür sorgen, dass Ihr JavaScript-Produktionscode für Nutzer so schnell wie möglich ist.

Demo für dynamischen Import

webpack

webpack wird mit einem Plug-in namens SplitChunksPlugin ausgeliefert, mit dem Sie konfigurieren können, wie der Bundler JavaScript-Dateien aufteilt. webpack erkennt sowohl die dynamischen import()- als auch die statischen import-Anweisungen. Das Verhalten von SplitChunksPlugin kann durch Angabe der Option chunks in der Konfiguration geändert werden:

  • chunks: async ist der Standardwert und bezieht sich auf dynamische import()-Aufrufe.
  • chunks: initial bezieht sich auf statische import-Aufrufe.
  • chunks: all umfasst sowohl dynamische import()- als auch statische Importe. So können Sie Chunks zwischen async- und initial-Importen freigeben.

Standardmäßig erstellt webpack immer dann, wenn es auf eine dynamische import()-Anweisung trifft, einen separaten Chunk für dieses Modul:

/* main.js */

// An application-specific chunk required during the initial page load:
import myFunction from './my-function.js';

myFunction('Hello world!');

// If a specific condition is met, a separate chunk is downloaded on demand,
// rather than being bundled with the initial chunk:
if (condition) {
  // Assumes top-level await is available. More info:
  // https://v8.dev/features/top-level-await
  await import('/form-validation.js');
}

Die Standard-Webpack-Konfiguration für den obigen Code-Snippet führt zu zwei separaten Chunks:

  • Der main.js-Chunk, den webpack als initial-Chunk klassifiziert, der das main.js- und das ./my-function.js-Modul enthält.
  • Der async-Chunk, der nur form-validation.js enthält (mit einem Dateihash im Ressourcennamen, falls konfiguriert). Dieser Chunk wird nur heruntergeladen, wenn condition wahr ist.

Mit dieser Konfiguration können Sie das Laden des form-validation.js-Chunks verzögern, bis er tatsächlich benötigt wird. So kann die Reaktionsfähigkeit beim Laden verbessert werden, da die Zeit für die Scriptauswertung beim ersten Seitenaufbau verkürzt wird. Das Herunterladen und Auswerten des Scripts für den form-validation.js-Chunk erfolgt, wenn eine bestimmte Bedingung erfüllt ist. In diesem Fall wird das dynamisch importierte Modul heruntergeladen. Ein Beispiel hierfür ist eine Bedingung, bei der ein Polyfill nur für einen bestimmten Browser heruntergeladen wird, oder – wie im vorherigen Beispiel – das importierte Modul für eine Nutzerinteraktion erforderlich ist.

Wenn Sie die SplitChunksPlugin-Konfiguration ändern, um chunks: initial anzugeben, wird der Code nur in den ersten Chunks aufgeteilt. Das sind Chunks wie die statisch importierten oder die in der entry-Property von webpack aufgeführten. Im vorherigen Beispiel wäre der resultierende Chunk eine Kombination aus form-validation.js und main.js in einer einzelnen Scriptdatei, was möglicherweise zu einer schlechteren Leistung beim ersten Seitenaufbau führt.

Die Optionen für SplitChunksPlugin können auch so konfiguriert werden, dass größere Skripts in mehrere kleinere aufgeteilt werden. Dazu kann beispielsweise die Option maxSize verwendet werden, um webpack anzuweisen, Chunks in separate Dateien aufzuteilen, wenn sie den durch maxSize angegebenen Wert überschreiten. Das Aufteilen großer Scriptdateien in kleinere Dateien kann die Reaktionsfähigkeit beim Laden verbessern, da in einigen Fällen die CPU-intensive Scriptauswertung in kleinere Aufgaben aufgeteilt wird, die den Hauptthread weniger wahrscheinlich über längere Zeiträume blockieren.

Außerdem bedeutet das Generieren größerer JavaScript-Dateien, dass die Wahrscheinlichkeit einer Cache-Entwertung bei Scripts steigt. Wenn Sie beispielsweise ein sehr großes Script mit Framework- und Erstanbieteranwendungscode ausliefern, kann das gesamte Bundle ungültig werden, wenn nur das Framework aktualisiert wird, aber nichts anderes in der gebündelten Ressource.

Andererseits erhöht sich durch kleinere Scriptdateien die Wahrscheinlichkeit, dass ein wiederkehrender Besucher Ressourcen aus dem Cache abruft. Das führt zu schnelleren Seitenladezeiten bei wiederholten Besuchen. Bei kleineren Dateien ist der Nutzen der Komprimierung jedoch geringer als bei größeren Dateien. Außerdem kann die Roundtrip-Zeit im Netzwerk beim Laden von Seiten mit einem nicht vorbereiteten Browsercache zunehmen. Es muss ein Gleichgewicht zwischen Caching-Effizienz, Komprimierungseffektivität und Skriptauswertungszeit gefunden werden.

webpack-Demo

webpack SplitChunksPlugin demo.

Wissen testen

Welche Art von import-Anweisung wird beim Aufteilen von Code verwendet?

Dynamische import().
Richtig!
Statisch import.
Bitte versuchen Sie es noch einmal.

Welche Art von import-Anweisung muss am Anfang eines JavaScript-Moduls stehen und darf an keiner anderen Stelle verwendet werden?

Dynamische import().
Bitte versuchen Sie es noch einmal.
Statisch import.
Richtig!

Was ist bei der Verwendung von SplitChunksPlugin in webpack der Unterschied zwischen einem async- und einem initial-Chunk?

async-Chunks werden mit dynamischem import() geladen und initial-Chunks mit statischem import.
Richtig!
async-Chunks werden mit statischen import geladen und initial-Chunks mit dynamischen import().
Bitte versuchen Sie es noch einmal.

Als Nächstes: Lazy Loading von Bildern und <iframe>-Elementen

JavaScript ist zwar in der Regel ein relativ teurer Ressourcentyp, aber nicht der einzige, dessen Laden Sie verzögern können. Bild- und <iframe>-Elemente können an sich schon kostspielige Ressourcen sein. Ähnlich wie bei JavaScript können Sie das Laden von Bildern und <iframe>-Elementen verzögern. Das wird im nächsten Modul dieses Kurses erläutert.