JavaScript für Codeaufteilung

Das Laden großer JavaScript-Ressourcen beeinträchtigt die Seitengeschwindigkeit erheblich. Wenn Sie den JavaScript-Code in kleinere Teile aufteilen und nur das herunterladen, was beim Start einer Seite erforderlich ist, kann die Ladereaktionsfähigkeit erheblich verbessert werden. Dies wiederum kann die Interaction to Next Paint (INP) Ihrer Seite verbessern.

Wenn eine Seite große JavaScript-Dateien herunterlädt, parst und kompiliert, kann sie zeitweise nicht mehr reagieren. Die Seitenelemente sind sichtbar, da sie Teil des ursprünglichen HTML-Codes einer Seite sind und mit CSS gestaltet sind. Der JavaScript-Code, der für diese interaktiven Elemente und andere von der Seite geladene Skripts erforderlich ist, kann jedoch geparst und ausgeführt werden, damit sie funktionieren. Dies kann dazu führen, dass der Nutzer das Gefühl hat, dass die Interaktion erheblich verzögert oder sogar komplett unterbrochen war.

Dies liegt häufig daran, dass der Hauptthread blockiert wird, da JavaScript im Hauptthread geparst und kompiliert wird. Wenn dieser Vorgang zu lange dauert, reagieren interaktive Seitenelemente unter Umständen nicht schnell genug auf Nutzereingaben. Eine Abhilfemaßnahme besteht darin, nur das JavaScript zu laden, das Sie für die Funktion der Seite benötigen, während anderes JavaScript durch eine Technik, die als Codeaufteilung bezeichnet wird, später geladen wird. In diesem Modul geht es um die letztere dieser beiden Techniken.

JavaScript-Parsing und -Ausführung während des Starts 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 mehr als 3,5 Sekunden dauert. Ein übermäßiges Parsing und eine übermäßige JavaScript-Ausführung ist ein potenzielles Problem an jedem Zeitpunkt im Seitenlebenszyklus, da es die Eingabeverzögerung einer Interaktion erhöhen kann, wenn der Zeitpunkt, zu dem der Nutzer mit der Seite interagiert, mit dem Zeitpunkt übereinstimmt, an dem die Hauptthreadaufgaben ausgeführt werden, die für die Verarbeitung und Ausführung von JavaScript verantwortlich sind.

Darüber hinaus ist eine übermäßige JavaScript-Ausführung und -Analyse beim ersten Seitenaufbau besonders problematisch, da dies der Punkt im Seitenlebenszyklus ist, an dem Nutzer mit der Seite sehr wahrscheinlich interagieren. Tatsächlich steht die Total Blocking Time (TBT) – ein Messwert für die Ladereaktionszeit – in einer hohen Korrelation mit der INP. Dies deutet darauf hin, dass Nutzer beim ersten Seitenaufbau sehr häufig Interaktionen ausführen.

Bei der Lighthouse-Prüfung wird die Zeit erfasst, die für die Ausführung der einzelnen JavaScript-Dateien benötigt wird, die Ihre Seitenanfragen ausführen. So können Sie genau ermitteln, welche Skripts für die Codeaufteilung infrage kommen. Mit dem Coverage-Tool in den Chrome-Entwicklertools lässt sich außerdem genau ermitteln, welche Teile des JavaScript-Codes einer Seite beim Seitenaufbau nicht verwendet werden.

Die Codeaufteilung ist eine nützliche Methode, um die anfänglichen JavaScript-Nutzlasten einer Seite zu reduzieren. Sie können ein JavaScript-Bundle in zwei Teile aufteilen:

  • Der JavaScript-Code, der beim Seitenaufbau erforderlich ist und daher zu keinem anderen Zeitpunkt geladen werden kann.
  • Verbleibendes JavaScript, das zu einem späteren Zeitpunkt geladen werden kann, meistens dann, wenn der Nutzer mit einem bestimmten interaktiven Element auf der Seite interagiert.

Die Codeaufteilung kann mithilfe der dynamischen import()-Syntax erfolgen. Im Gegensatz zu <script>-Elementen, die beim Start eine bestimmte JavaScript-Ressource anfordern, sendet diese Syntax später im Seitenlebenszyklus eine Anfrage für eine JavaScript-Ressource.

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 Modul validate-form.mjs nur dann heruntergeladen, geparst und ausgeführt, wenn ein Nutzer eines der <input>-Felder eines Formulars unkenntlich macht. In diesem Fall ist die JavaScript-Ressource für die Validierungslogik des Formulars nur dann an der Seite beteiligt, wenn sie am wahrscheinlichsten tatsächlich verwendet wird.

JavaScript-Bundler wie webpack, Parcel, Rollup und esbuild können so konfiguriert werden, dass JavaScript-Bundles in kleinere Blöcke aufgeteilt werden, wenn in Ihrem Quellcode ein dynamischer Aufruf von import() auftritt. Die meisten dieser Tools tun dies automatisch, aber besonders bei Esbuild müssen Sie diese Optimierung aktivieren.

Hilfreiche Hinweise zur Codeaufteilung

Die Codeaufteilung ist eine effektive Methode, um Konflikte zwischen Hauptthreads beim ersten Seitenaufbau zu reduzieren. Es zahlt sich jedoch aus, einige Punkte zu beachten, wenn Sie Ihren JavaScript-Quellcode auf Möglichkeiten zur Codeaufteilung prüfen möchten.

Wenn möglich, einen Bundler verwenden

Entwickler verwenden während des Entwicklungsprozesses üblicherweise JavaScript-Module. Es handelt sich um eine hervorragende Verbesserung für die Entwicklung von Code, die die Lesbarkeit und Verwaltbarkeit von Code verbessert. Es gibt jedoch einige suboptimale Leistungsmerkmale, die sich beim Versand von JavaScript-Modulen in die Produktion ergeben können.

Am wichtigsten ist, dass Sie einen Bundler zum Verarbeiten und Optimieren Ihres Quellcodes verwenden, einschließlich der Module, die Sie aufteilen möchten. Bundler sind sehr effektiv, um nicht nur Optimierungen auf den JavaScript-Quellcode vorzunehmen, sondern auch, um Leistungsaspekte wie die Bundle-Größe und das Komprimierungsverhältnis in Einklang zu bringen. Die Effektivität der Komprimierung nimmt mit der Bundle-Größe zu. Bundler versuchen jedoch auch, sicherzustellen, dass Bundles nicht so groß sind, dass sie aufgrund der Skriptauswertung lange Aufgaben verursachen.

Bundler vermeiden außerdem das Problem, eine große Anzahl nicht gebündelter Module über das Netzwerk zu senden. Architekturen, die JavaScript-Module verwenden, haben meist große, komplexe Modulbäume. Wenn Modulstrukturen entbündelt sind, stellt jedes Modul eine separate HTTP-Anfrage dar. Die Interaktivität in Ihrer Webanwendung kann sich verzögern, wenn Sie keine Module bündeln. Es ist zwar möglich, den <link rel="modulepreload">-Ressourcenhinweis zu verwenden, um große Modulbäume so früh wie möglich zu laden, JavaScript-Bundles sind aber im Hinblick auf die Ladeleistung dennoch besser geeignet.

Deaktivieren Sie nicht versehentlich die Streamingkompilierung.

Die V8-JavaScript-Engine von Chromium bietet eine Reihe sofort verfügbarer Optimierungen, damit 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-Code, der an den Browser gestreamt wird, gestreamte JavaScript-Chunks kompiliert, sobald diese aus dem Netzwerk eintreffen.

Es gibt verschiedene Möglichkeiten, um sicherzustellen, dass die Streamingkompilierung für Ihre Webanwendung in Chromium erfolgt:

  • Transformiere deinen Produktionscode, um die Verwendung von JavaScript-Modulen zu vermeiden. Bundler können Ihren JavaScript-Quellcode auf der Grundlage eines Kompilierungsziels transformieren, wobei das Ziel oft umgebungsspezifisch ist. V8 wendet die Streamingkompilierung auf jeden JavaScript-Code an, der keine Module verwendet, und Sie können Ihren Bundler so konfigurieren, dass Ihr JavaScript-Modulcode in eine Syntax umgewandelt wird, die keine JavaScript-Module und deren Funktionen verwendet.
  • Wenn du JavaScript-Module an die Produktion senden möchtest, verwende die Erweiterung .mjs. Unabhängig davon, ob Ihr Produktions-JavaScript Module verwendet oder nicht, gibt es im Gegensatz zu JavaScript keinen speziellen Inhaltstyp für JavaScript, bei dem Module verwendet werden. Bei V8 deaktivieren Sie praktisch die Streamingkompilierung, wenn Sie JavaScript-Module mit der Erweiterung .js in der Produktion versenden. Wenn Sie die Erweiterung .mjs für JavaScript-Module verwenden, sorgt V8 dafür, dass die Streamingkompilierung für modulbasierten JavaScript-Code nicht fehlerhaft ist.

Diese Überlegungen sollten Sie nicht von der Codeaufteilung abschrecken. Die Codeaufteilung ist eine effektive Möglichkeit, die anfänglichen JavaScript-Nutzlasten für Nutzer zu 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.

Dynamischer Import – Demo

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. Sie können das Verhalten von SplitChunksPlugin ändern, indem Sie in der Konfiguration die Option chunks angeben:

  • chunks: async ist der Standardwert und bezieht sich auf dynamische import()-Aufrufe.
  • chunks: initial bezieht sich auf statische import-Aufrufe.
  • chunks: all deckt sowohl dynamische import()- als auch statische Importe ab, sodass Sie Blöcke für async- und initial-Importe teilen können.

Wenn Webpack eine dynamische import()-Anweisung stößt, wird standardmäßig ein separater Chunk für dieses Modul erstellt:

/* 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 das vorherige Code-Snippet führt zu zwei separaten Blöcken:

  • Der main.js-Chunk, der von Webpack als initial-Chunk klassifiziert wird, der die Module main.js und ./my-function.js enthält.
  • Den async-Chunk, der nur form-validation.js enthält (mit einem Datei-Hash im Ressourcennamen, sofern konfiguriert). Dieser Chunk wird nur heruntergeladen, wenn condition truthy ist.

Mit dieser Konfiguration können Sie das Laden des Chunks form-validation.js auf später verschieben, bis er tatsächlich benötigt wird. Dies kann die Reaktionszeit beim Laden verbessern, indem die Skriptauswertung beim ersten Seitenaufbau verkürzt wird. Das Herunterladen und die Auswertung des Skripts für den Block form-validation.js erfolgt, wenn eine angegebene Bedingung erfüllt ist. In diesem Fall wird das dynamisch importierte Modul heruntergeladen. Ein Beispiel kann eine Bedingung sein, bei der ein Polyfill nur für einen bestimmten Browser heruntergeladen wird. Oder das importierte Modul ist wie im vorherigen Beispiel für eine Nutzerinteraktion erforderlich.

Wenn Sie dagegen die SplitChunksPlugin-Konfiguration so ändern, dass chunks: initial angegeben wird, wird der Code nur in den ersten Blöcken aufgeteilt. Dabei handelt es sich beispielsweise um Blöcke, die statisch importiert oder im Webpack-Attribut entry aufgelistet werden. Im obigen Beispiel würde der resultierende Block eine Kombination aus form-validation.js und main.js in einer einzelnen Skriptdatei sein, was zu einer potenziell schlechteren Leistung beim anfänglichen Seitenaufbau führen würde.

Die Optionen für SplitChunksPlugin können auch so konfiguriert werden, dass größere Skripts in mehrere kleinere Skripts aufgeteilt werden. Verwenden Sie beispielsweise die Option maxSize, um Webpack anzuweisen, Blöcke in separate Dateien aufzuteilen, wenn diese den Wert von maxSize überschreiten. Das Aufteilen großer Skriptdateien in kleinere Dateien kann die Reaktionsfähigkeit beim Laden verbessern, da CPU-intensive Skriptauswertungen in einigen Fällen in kleinere Aufgaben aufgeteilt werden, die den Hauptthread wahrscheinlich nicht über längere Zeiträume blockieren.

Außerdem führt das Erstellen größerer JavaScript-Dateien dazu, dass Skripts eher an einer Cache-Entwertung leiden. Wenn Sie beispielsweise ein sehr großes Skript sowohl mit dem Framework als auch mit dem Code der eigenen Anwendung versenden, kann das gesamte Bundle entwertet werden, wenn nur das Framework, aber nichts anderes in der gebündelten Ressource aktualisiert wird.

Andererseits erhöhen kleinere Skriptdateien die Wahrscheinlichkeit, dass ein wiederkehrender Besucher Ressourcen aus dem Cache abruft. Dadurch werden Seiten bei wiederholten Besuchen schneller geladen. Kleinere Dateien profitieren jedoch weniger von der Komprimierung als größere und können die Netzwerkumlaufzeit beim Seitenaufbau mit einem nicht verwalteten Browser-Cache erhöhen. Es muss darauf geachtet werden, ein Gleichgewicht zwischen Caching-Effizienz, Komprimierungseffektivität und Zeit für die Skriptauswertung zu finden.

Webpack-Demo

Webpack SplitChunksPlugin-Demo

Wissen testen

Welche Art von import-Anweisung wird bei der Codeaufteilung verwendet?

Dynamische import().
Richtig!
Statisches import.
Versuche es bitte noch einmal.

Welche Art von import-Anweisung muss ganz oben in einem JavaScript-Modul und an keiner anderen Stelle stehen?

Dynamische import().
Versuche es bitte noch einmal.
Statisches import.
Richtig!

Was ist der Unterschied zwischen einem async-Chunk und einem initial-Chunk, wenn Sie SplitChunksPlugin in Webpack verwenden?

async-Chunks werden mit dynamischem import() und initial-Chunks mit statischem import geladen.
Richtig!
async-Chunks werden mit statischem import und initial-Chunks mit dynamischem import() geladen.
Versuche es bitte noch einmal.

Nächster Schritt: Lazy Loading von Bildern und <iframe>-Elementen

JavaScript ist nicht der einzige Ressourcentyp, mit dem das Laden aufgeschoben werden kann, auch wenn es sich normalerweise um einen relativ teuren Ressourcentyp handelt. Bild- und <iframe>-Elemente sind potenziell kostspielige Ressourcen. Ähnlich wie bei JavaScript können Sie das Laden von Bildern und <iframe>-Elementen durch Lazy Loading verzögern. Dies wird im nächsten Modul dieses Kurses beschrieben.