Verbesserte Next.js- und Gatsby-Seitenladeleistung durch detailliertes Aufteilen

Eine neuere webpack-Chunking-Strategie in Next.js und Gatsby minimiert doppelten Code, um die Leistung beim Laden der Seite zu verbessern.

Chrome arbeitet mit Tools und Frameworks im Open-Source-Ökosystem für JavaScript zusammen. Kürzlich wurden einige neue Optimierungen hinzugefügt, um die Ladeleistung von Next.js und Gatsby zu verbessern. In diesem Artikel wird eine verbesserte Strategie für die detaillierte Aufteilung beschrieben, die jetzt standardmäßig in beiden Frameworks verwendet wird.

Einführung

Wie viele Web-Frameworks verwenden Next.js und Gatsby webpack als Core Bundle. Mit Webpack v3 wurde CommonsChunkPlugin eingeführt, um die Ausgabe von Modulen zu ermöglichen, die zwischen verschiedenen Einstiegspunkten in einem oder mehreren „Commons“-Blocks (oder Blöcken) gemeinsam genutzt werden. Freigegebener Code kann separat heruntergeladen und frühzeitig im Browsercache gespeichert werden, was zu einer besseren Ladeleistung führen kann.

Dieses Muster wurde bei vielen Single-Page-Anwendungs-Frameworks beliebt, die eine Einstiegspunkt- und Bundle-Konfiguration wie diese verwendeten:

Gemeinsame Konfiguration von Einstiegspunkt und Bundle

Das Konzept, den gesamten Code aus gemeinsam genutzten Modulen in einem einzigen Block zu bündeln, ist zwar praktisch, hat aber seine Grenzen. Module, die nicht an allen Einstiegspunkten freigegeben werden, können für Routen heruntergeladen werden, in denen sie nicht verwendet werden. Dadurch wird mehr Code heruntergeladen als nötig. Wenn beispielsweise page1 den Block common lädt, wird der Code für moduleC geladen, obwohl page1 nicht moduleC verwendet. Aus diesem Grund wurde das Plugin in Webpack v4 zusammen mit einigen anderen entfernt und durch ein neues ersetzt: SplitChunksPlugin.

Verbesserte Chunking-Methode

Die Standardeinstellungen für SplitChunksPlugin eignen sich für die meisten Nutzer. Je nach Bedingungen werden mehrere geteilte Chunks erstellt, um zu verhindern, dass duplizierter Code über mehrere Routen abgerufen wird.

Viele Web-Frameworks, die dieses Plug-in verwenden, verfolgen jedoch immer noch einen Single-Commons-Ansatz für die Blockaufteilung. Next.js würde beispielsweise ein commons-Bundle generieren, das alle Module enthält, die auf mehr als 50 % der Seiten verwendet werden, sowie alle Framework-Abhängigkeiten (react, react-dom usw.).

const splitChunksConfigs = {
  
  prod: {
    chunks: 'all',
    cacheGroups: {
      default: false,
      vendors: false,
      commons: {
        name: 'commons',
        chunks: 'all',
        minChunks: totalPages > 2 ? totalPages * 0.5 : 2,
      },
      react: {
        name: 'commons',
        chunks: 'all',
        test: /[\\/]node_modules[\\/](react|react-dom|scheduler|use-subscription)[\\/]/,
      },
    },
  },

Auch wenn das Einbinden von Framework-abhängigem Code in einen gemeinsamen Block bedeutet, dass dieser für jeden Einstiegspunkt heruntergeladen und im Cache gespeichert werden kann, ist die nutzungsbasierte Heuristik, bei der gängige Module, die in mehr als Hälften der Seiten verwendet werden, nicht sehr effektiv. Eine Änderung dieses Verhältnisses kann nur zu einem von zwei Ergebnissen führen:

  • Wenn Sie das Verhältnis reduzieren, wird mehr unnötiger Code heruntergeladen.
  • Wenn Sie das Verhältnis erhöhen, wird mehr Code in mehreren Routen dupliziert.

Zur Lösung dieses Problems verwendete Next.js eine andere Konfiguration für SplitChunksPlugin, die unnötigen Code für jede Route reduziert.

  • Alle ausreichend großen Drittanbietermodule (größer als 160 KB) werden in einen eigenen einzelnen Chunk aufgeteilt.
  • Für Framework-Abhängigkeiten wird ein separater frameworks-Chunk erstellt (react, react-dom usw.).
  • Es werden so viele freigegebene Blöcke wie nötig erstellt (bis zu 25).
  • Die Mindestgröße für einen zu generierenden Chunk wurde auf 20 KB geändert.

Diese detaillierte Aufteilung bietet folgende Vorteile:

  • Die Seitenladezeiten werden verbessert. Wenn Sie mehrere gemeinsame Chunks anstelle eines einzelnen ausgeben, wird die Menge an unnötigem (oder dupliziertem) Code für jeden Einstiegspunkt minimiert.
  • Verbessertes Caching während der Navigation Wenn Sie große Bibliotheken und Framework-Abhängigkeiten in separate Blöcke aufteilen, verringert sich die Wahrscheinlichkeit, dass der Cache ungültig wird, da sich beides bis zu einem Upgrade wahrscheinlich nicht ändert.

Die gesamte von Next.js übernommene Konfiguration finden Sie unter webpack-config.ts.

Mehr HTTP-Anfragen

SplitChunksPlugin hat die Grundlage für detailliertes Chunking definiert. Die Anwendung dieses Ansatzes auf ein Framework wie Next.js war also kein völlig neues Konzept. Viele Frameworks verwendeten jedoch aus verschiedenen Gründen weiterhin eine einzelne Heuristik und eine „Commons“-Bundle-Strategie. Dazu gehört auch die Sorge, dass viel mehr HTTP-Anfragen die Websiteleistung beeinträchtigen können.

Browser können nur eine begrenzte Anzahl von TCP-Verbindungen zu einem einzigen Ursprung öffnen (6 für Chrome). Wenn Sie also die Anzahl der von einem Bundler ausgegebenen Blöcke minimieren, können Sie dafür sorgen, dass die Gesamtzahl der Anfragen unter diesem Grenzwert bleibt. Dies gilt jedoch nur für HTTP/1.1. Beim Multiplexing in HTTP/2 können mehrere Anfragen parallel mit einer einzigen Verbindung über einen einzigen Ursprung gestreamt werden. Mit anderen Worten, wir müssen uns im Allgemeinen keine Gedanken über die Begrenzung der Anzahl von Blöcken machen, die von unserem Bundler ausgegeben werden.

Alle gängigen Browser unterstützen HTTP/2. Die Chrome- und Next.js-Teams wollten herausfinden, ob sich die Ladeleistung durch eine Erhöhung der Anzahl der Anfragen beeinträchtigen lässt, wenn das einzelne „commons“-Bundle von Next.js in mehrere gemeinsame Chunks aufgeteilt wird. Zuerst wurde die Leistung einer einzelnen Website gemessen und die maximale Anzahl paralleler Anfragen mithilfe des Attributs maxInitialRequests geändert.

Seitenladezeit bei erhöhter Anzahl von Anfragen

Bei durchschnittlich drei Durchläufen mehrerer Tests auf einer einzelnen Webseite blieben die Zeiten für load, Start-Render und First Contentful Paint ungefähr gleich, wenn die maximale Anzahl der ersten Anfragen variiert wurde (von 5 auf 15). Interessanterweise haben wir erst nach einer aggressiven Aufteilung auf Hunderte von Anfragen einen leichten Leistungsaufwand festgestellt.

Leistung beim Seitenaufbau mit Hunderten von Anfragen

Dies zeigte, dass das Halten eines zuverlässigen Schwellenwerts (20~25 Anfragen) das richtige Gleichgewicht zwischen Ladeleistung und Caching-Effizienz schaffte. Nach einigen Vorabtests wurde 25 als Anzahl der maxInitialRequest ausgewählt.

Die Änderung der maximalen Anzahl parallel gestellter Anfragen führte zu mehr als einem freigegebenen Bundle. Durch die entsprechende Trennung für die einzelnen Einstiegspunkte konnte die Menge an unnötigem Code für dieselbe Seite erheblich reduziert werden.

Verringerung der JavaScript-Nutzlast durch mehr Chunking

Bei diesem Test ging es nur darum, die Anzahl der Anfragen zu ändern, um zu sehen, ob sich dies negativ auf die Seitenladeleistung auswirkt. Die Ergebnisse deuten darauf hin, dass die Einstellung von maxInitialRequests auf 25 auf der Testseite optimal war, da dadurch die Größe der JavaScript-Nutzlast reduziert wurde, ohne die Seite zu verlangsamen. Die Gesamtmenge an JavaScript, die zum Hydratisieren der Seite erforderlich war, blieb ungefähr gleich. Das erklärt, warum sich die Leistung beim Laden der Seite durch die reduzierte Codemenge nicht unbedingt verbesserte.

Webpack verwendet standardmäßig 30 KB als Mindestgröße für die Generierung eines Blocks. Die Kombination eines maxInitialRequests-Werts von 25 mit einer Mindestgröße von 20 KB führte jedoch zu einem besseren Caching.

Größenreduzierungen mit detaillierten Blöcken

Viele Frameworks, einschließlich Next.js, nutzen das clientseitige Routing (von JavaScript verarbeitet), um bei jeder Routenübergang neue Script-Tags einzufügen. Aber wie werden diese dynamischen Chunks zum Zeitpunkt der Erstellung festgelegt?

Next.js verwendet eine serverseitige Build-Manifestdatei, um zu bestimmen, welche Ausgabe-Chunks von verschiedenen Einstiegspunkten verwendet werden. Um diese Informationen auch dem Kunden zur Verfügung zu stellen, wurde eine gekürzte clientseitige Build-Manifestdatei erstellt, in der alle Abhängigkeiten für jeden Einstiegspunkt zugeordnet sind.

// Returns a promise for the dependencies for a particular route
getDependencies (route) {
  return this.promisedBuildManifest.then(
    man => (man[route] && man[route].map(url => `/_next/${url}`)) || []
  )
}
Ausgabe mehrerer gemeinsam genutzter Chunks in einer Next.js-Anwendung.

Diese neuere Strategie für detailliertes Chunking wurde zuerst in Next.js mit einem Flag eingeführt und dort an einer Reihe von Early Adoptern getestet. Viele konnten die Gesamtmenge des für ihre gesamte Website verwendeten JavaScripts deutlich reduzieren:

Website JS-Gesamtänderung Unterschied in %
https://www.barnebys.com/ -238 KB -23 %
https://sumup.com/ − 220 KB -30 %
https://www.hashicorp.com/ – 11 MB -71%
Verringerung der JavaScript-Größe – für alle Pfade (komprimiert)

Die finale Version wurde standardmäßig in Version 9.2 ausgeliefert.

Gatsby

Gatsby folgte demselben Ansatz, bei dem zum Definieren gängiger Module eine nutzungsbasierte Heuristik verwendet wird:

config.optimization = {
  
  splitChunks: {
    name: false,
    chunks: `all`,
    cacheGroups: {
      default: false,
      vendors: false,
      commons: {
        name: `commons`,
        chunks: `all`,
        // if a chunk is used more than half the components count,
        // we can assume it's pretty global
        minChunks: componentsCount > 2 ? componentsCount * 0.5 : 2,
      },
      react: {
        name: `commons`,
        chunks: `all`,
        test: /[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/,
      },

Durch die Optimierung der Webpack-Konfiguration für eine ähnliche detaillierte Segmentierungsstrategie stellte das Unternehmen auch erhebliche JavaScript-Reduzierungen bei vielen großen Websites fest:

Website JS-Gesamtänderung Unterschied in %
https://www.gatsbyjs.org/ – 680 KB -22%
https://www.thirdandgrove.com/ -390 KB -25 %
https://ghost.org/ -1,1 MB -35 %
https://reactjs.org/ -80 KB -8 %
Verringerung der JavaScript-Größe – für alle Pfade (komprimiert)

In der PR-Dokumentation erfährst du, wie das Unternehmen diese Logik in die Webpack-Konfiguration implementiert hat, die standardmäßig in Version 2.20.7 ausgeliefert wird.

Fazit

Das Konzept des Versands von detaillierten Chunks ist nicht spezifisch für Next.js, Gatsby oder webpack. Unabhängig vom verwendeten Framework oder Modul-Bundler sollten Sie die Chunking-Strategie Ihrer Anwendung verbessern, wenn sie dem Ansatz eines großen „Commons“-Bundles folgt.

  • Wenn Sie sehen möchten, wie dieselben Caching-Optimierungen auf eine Standard-React-Anwendung angewendet werden, sehen Sie sich diese Beispiel-React-App an. Sie verwendet eine vereinfachte Version der detaillierten Caching-Strategie und kann Ihnen dabei helfen, dieselbe Logik auf Ihre Website anzuwenden.
  • Bei der Zusammenstellung werden die Chunks standardmäßig detailliert erstellt. Unter manualChunks können Sie das Verhalten manuell konfigurieren.