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

Eine neuere Webpack-Chunking-Strategie in Next.js und Gatsby minimiert doppelten Code, um die Seitenladeleistung zu verbessern.

Chrome arbeitet mit Tools und Frameworks im JavaScript-Open-Source-Ökosystem zusammen. Vor Kurzem wurden eine Reihe neuerer Optimierungen hinzugefügt, um die Ladeleistung von Next.js und Gatsby zu verbessern. In diesem Artikel wird eine verbesserte granulare Chunking-Strategie beschrieben, die jetzt standardmäßig in beiden Frameworks enthalten ist.

Einführung

Wie viele Web-Frameworks verwenden Next.js und Gatsby webpack als zentralen Bundler. Mit webpack v3 wurde CommonsChunkPlugin eingeführt, um Module, die zwischen verschiedenen Einstiegspunkten geteilt werden, in einem oder mehreren „commons“-Chunks auszugeben. Gemeinsam genutzter 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 verwenden:

Gängige Einstiegspunkt- und Bundle-Konfiguration

Das Konzept, den gesamten freigegebenen Modulcode in einem einzigen Chunk zu bündeln, ist zwar praktisch, hat aber auch seine Grenzen. Module, die nicht in jedem Einstiegspunkt freigegeben sind, können für Routen heruntergeladen werden, die sie nicht verwenden. Das führt dazu, dass mehr Code als nötig heruntergeladen wird. Wenn beispielsweise page1 den Chunk common lädt, wird der Code für moduleC geladen, obwohl page1 moduleC nicht verwendet. Aus diesem und einigen anderen Gründen wurde das Plugin in webpack v4 zugunsten eines neuen Plugins entfernt: SplitChunksPlugin.

Verbessertes Chunking

Die Standardeinstellungen für SplitChunksPlugin sind für die meisten Nutzer gut geeignet. Je nach einer Reihe von Bedingungen werden mehrere aufgeteilte Chunks erstellt, um zu verhindern, dass doppelter Code über mehrere Routen hinweg abgerufen wird.

Viele Web-Frameworks, die dieses Plug-in verwenden, folgen jedoch weiterhin einem „Single-Commons“-Ansatz für das Aufteilen von Chunks. 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)[\\/]/,
      },
    },
  },

Wenn Sie frameworkabhängigen Code in einen gemeinsamen Chunk aufnehmen, kann er für jeden Einstiegspunkt heruntergeladen und im Cache gespeichert werden. Die nutzungsbasierte Heuristik, die gemeinsame Module einschließt, die auf mehr als der Hälfte der Seiten verwendet werden, ist jedoch nicht sehr effektiv. Eine Änderung dieses Verhältnisses hätte nur eine von zwei Folgen:

  • Wenn Sie das Verhältnis verringern, wird mehr unnötiger Code heruntergeladen.
  • Wenn Sie das Verhältnis erhöhen, wird mehr Code über mehrere Routen hinweg dupliziert.

Um dieses Problem zu beheben, hat Next.js eine andere Konfiguration fürSplitChunksPlugin eingeführt, die unnötigen Code für jede Route reduziert.

  • Jedes ausreichend große Drittanbietermodul (über 160 KB) wird in einen eigenen Chunk aufgeteilt.
  • Für Framework-Abhängigkeiten (react, react-dom usw.) wird ein separater frameworks-Chunk erstellt.
  • Es werden so viele gemeinsame Chunks wie nötig erstellt (bis zu 25).
  • Die Mindestgröße für die Generierung eines Chunks wurde auf 20 KB geändert.

Diese Strategie für die granulare Aufteilung bietet folgende Vorteile:

  • Seitenladezeiten wurden verbessert: Wenn mehrere freigegebene Chunks anstelle eines einzelnen ausgegeben werden, wird die Menge an unnötigem oder doppeltem Code für jeden Einstiegspunkt minimiert.
  • Verbessertes Caching während der Navigation: Wenn Sie große Bibliotheken und Framework-Abhängigkeiten in separate Chunks aufteilen, verringert sich die Wahrscheinlichkeit einer Cache-Invalidierung, da sich beide erst bei einem Upgrade ändern.

Die gesamte Konfiguration, die Next.js übernommen hat, finden Sie unter webpack-config.ts.

Mehr HTTP-Anfragen

SplitChunksPlugin bildete die Grundlage für die granulare Aufteilung in Chunks. Die Anwendung dieses Ansatzes auf ein Framework wie Next.js war kein völlig neues Konzept. Viele Frameworks verwenden jedoch aus mehreren Gründen weiterhin eine einzelne Heuristik und eine „commons“-Bundle-Strategie. Dazu gehört auch die Befürchtung, dass viele zusätzliche HTTP-Anfragen die Websiteleistung negativ beeinflussen können.

Browser können nur eine begrenzte Anzahl von TCP-Verbindungen zu einem einzelnen Ursprung öffnen (6 für Chrome). Wenn Sie die Anzahl der von einem Bundler ausgegebenen Chunks 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. Multiplexing in HTTP/2 ermöglicht das parallele Streamen mehrerer Anfragen über eine einzige Verbindung zu einem einzelnen Ursprung. Mit anderen Worten: Wir müssen uns im Allgemeinen keine Gedanken darüber machen, die Anzahl der vom Bundler ausgegebenen Chunks zu begrenzen.

Alle wichtigen Browser unterstützen HTTP/2. Die Chrome- und Next.js-Teams wollten herausfinden, ob sich eine Erhöhung der Anzahl der Anfragen durch Aufteilen des einzelnen „commons“-Bundles von Next.js in mehrere gemeinsam genutzte Chunks auf die Ladeleistung auswirken würde. Zuerst haben sie die Leistung einer einzelnen Website gemessen und dabei die maximale Anzahl paralleler Anfragen mit der Eigenschaft maxInitialRequests geändert.

Seitenladeleistung bei erhöhter Anzahl von Anfragen

Bei durchschnittlich drei Durchläufen mit mehreren Tests auf einer einzelnen Webseite blieben die Zeiten für load, start-render und First Contentful Paint bei einer Änderung der maximalen Anzahl der ersten Anfragen (von 5 bis 15) ungefähr gleich. Interessanterweise haben wir einen leichten Leistungs-Overhead erst nach dem aggressiven Aufteilen in Hunderte von Anfragen festgestellt.

Seitenladeleistung bei Hunderten von Anfragen

Das hat gezeigt, dass ein zuverlässiger Grenzwert (20 bis 25 Anfragen) das richtige Gleichgewicht zwischen Ladeleistung und Caching-Effizienz bietet. Nach einigen Baseline-Tests wurde 25 als maxInitialRequest-Anzahl ausgewählt.

Durch die Änderung der maximalen Anzahl paralleler Anfragen wurde mehr als ein gemeinsames Bundle erstellt. Durch die entsprechende Trennung für jeden Einstiegspunkt wurde die Menge an unnötigem Code für dieselbe Seite erheblich reduziert.

Reduzierung der JavaScript-Nutzlast durch mehr Chunking

In diesem Test ging es nur darum, die Anzahl der Anfragen zu ändern, um zu sehen, ob sich dies negativ auf die Seitenladeleistung auswirken würde. 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 Hydrieren der Seite erforderlich war, blieb jedoch ungefähr gleich. Das erklärt, warum sich die Seitenladeleistung durch die geringere Menge an Code nicht unbedingt verbessert hat.

webpack verwendet 30 KB als Standardmindestgröße für die Generierung eines Chunks. Die Kombination eines maxInitialRequests-Werts von 25 mit einer Mindestgröße von 20 KB führte jedoch zu einem besseren Caching.

Größenreduzierungen mit granularen Chunks

Viele Frameworks, darunter Next.js, verwenden clientseitiges Routing (das von JavaScript verarbeitet wird), um bei jedem Routenübergang neuere Script-Tags einzufügen. Aber wie werden diese dynamischen Chunks zur Build-Zeit vorab festgelegt?

Next.js verwendet eine serverseitige Build-Manifestdatei, um zu ermitteln, welche ausgegebenen Chunks von verschiedenen Einstiegspunkten verwendet werden. Damit diese Informationen auch dem Client zur Verfügung stehen, wurde eine gekürzte clientseitige Build-Manifestdatei erstellt, um alle Abhängigkeiten für jeden Einstiegspunkt zuzuordnen.

// 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 neue, detailliertere Chunking-Strategie wurde zuerst in Next.js hinter einem Flag eingeführt und von einer Reihe von Early Adopters getestet. Viele konnten die Menge an JavaScript, die für ihre gesamte Website verwendet wird, deutlich reduzieren:

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

Die endgültige Version wurde standardmäßig in Version 9.2 ausgeliefert.

Gatsby

Gatsby hat früher denselben Ansatz verfolgt und eine nutzungsbasierte Heuristik zum Definieren gemeinsamer Module verwendet:

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 ihrer Webpack-Konfiguration, um eine ähnliche granulare Chunking-Strategie zu verwenden, konnten sie auch auf vielen großen Websites erhebliche JavaScript-Reduzierungen feststellen:

Website Gesamtänderung bei JS 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 %
Reduzierung der JavaScript-Größe – für alle Routen (komprimiert)

Im PR können Sie nachlesen, wie diese Logik in die Webpack-Konfiguration implementiert wurde, die standardmäßig in Version 2.20.7 enthalten ist.

Fazit

Das Konzept, granulare Chunks zu versenden, ist nicht spezifisch für Next.js, Gatsby oder sogar webpack. Jeder sollte die Chunking-Strategie seiner Anwendung verbessern, wenn sie einem großen „Commons“-Bundle-Ansatz folgt, unabhängig vom verwendeten Framework oder Modul-Bundler.

  • Wenn Sie sehen möchten, wie dieselben Optimierungen für das Chunking auf eine reine React-Anwendung angewendet werden, sehen Sie sich diese React-Beispielanwendung an. Sie verwendet eine vereinfachte Version der granularen Chunking-Strategie und kann Ihnen helfen, dieselbe Art von Logik auf Ihre Website anzuwenden.
  • Für Rollup werden standardmäßig detaillierte Chunks erstellt. Informationen zum manuellen Konfigurieren des Verhaltens finden Sie unter manualChunks.