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 Seitenaufbau zu verbessern.

Chrome arbeitet mit Tools und Frameworks im JavaScript-Open-Source-Ökosystem zusammen. Vor Kurzem wurden einige neuere 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 Kern-Bundler. Mit webpack v3 wurde CommonsChunkPlugin eingeführt, um Module, die von verschiedenen Einstiegspunkten gemeinsam genutzt werden, in einem oder mehreren „Commons“-Chunks auszugeben. Freigegebener Code kann separat heruntergeladen und frühzeitig im Browser-Cache 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:

Allgemeine Einstiegspunkt- und Bundle-Konfiguration

Das Konzept, den gesamten Code gemeinsam genutzter Module 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. Dadurch wird mehr Code als nötig heruntergeladen. Wenn page1 beispielsweise 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 Plug-in in webpack v4 durch ein neues ersetzt: SplitChunksPlugin.

Verbessertes Chunking

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

Viele Web-Frameworks, die dieses Plug-in verwenden, folgen jedoch immer noch einem „Single-Commons“-Ansatz für die Aufteilung 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 freigegebenen Chunk einfügen, kann er für jeden Einstiegspunkt heruntergeladen und im Cache gespeichert werden. Die nutzungsbasierte Heuristik, bei der gängige Module, die auf mehr als der Hälfte der Seiten verwendet werden, einbezogen werden, ist jedoch nicht sehr effektiv. Eine Änderung dieses Verhältnisses würde nur zu einem von zwei Ergebnissen führen:

  • 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, wurde in Next.js eine andere Konfiguration für SplitChunksPlugin eingeführt, die unnötigen Code für alle Routen reduziert.

  • Jedes ausreichend große Drittanbietermodul (größer als 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 freigegebene Chunks wie nötig erstellt (bis zu 25).
  • Die Mindestgröße für die Generierung eines Chunks wird auf 20 KB geändert.

Diese granulare Chunking-Strategie bietet folgende Vorteile:

  • Die Seitenladezeiten werden verbessert. Wenn mehrere freigegebene Chunks statt 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 Blöcke aufteilen, verringert sich die Wahrscheinlichkeit einer Cache-Entwertung, da sich beide wahrscheinlich erst nach einem Upgrade ändern.

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

Mehr HTTP-Anfragen

SplitChunksPlugin hat die Grundlage für die granulare Aufteilung definiert. Die Anwendung dieses Ansatzes auf ein Framework wie Next.js war kein völlig neues Konzept. Viele Frameworks verwendeten jedoch aus verschiedenen Gründen weiterhin eine einzige Heuristik und eine „Commons“-Bundle-Strategie. Dazu gehört auch die Sorge, dass sich viele weitere HTTP-Anfragen negativ auf die Websiteleistung auswirken 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 Schwellenwert 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 und einen einzigen Ursprung. Mit anderen Worten: Wir müssen uns im Allgemeinen keine Gedanken darüber machen, die Anzahl der von unserem Bundler ausgegebenen Chunks zu begrenzen.

Alle gängigen Browser unterstützen HTTP/2. Die Chrome- und Next.js-Teams wollten herausfinden, ob sich die Erhöhung der Anzahl der Anfragen durch die Aufteilung des einzelnen „Commons“-Bundles von Next.js in mehrere freigegebene Chunks auf die Ladeleistung auswirkt. Zuerst wurde die Leistung einer einzelnen Website gemessen und dabei die maximale Anzahl paralleler Anfragen mit dem Attribut maxInitialRequests geändert.

Seitenaufbauleistung bei erhöhter Anzahl von Anfragen

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

Seitenaufbauleistung bei Hunderten von Anfragen

Dabei hat sich gezeigt, dass ein zuverlässiger Schwellenwert (20–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 wurden mehrere freigegebene Bundles 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 verstärktes Chunking

In diesem Test ging es nur darum, die Anzahl der Anfragen zu ändern, um zu sehen, ob sich dies negativ auf die Leistung beim Seitenaufbau auswirkt. Die Ergebnisse deuten darauf hin, dass die Einstellung von maxInitialRequests auf 25 auf der Testseite optimal war, da die Größe der JavaScript-Nutzlast reduziert wurde, ohne die Seite zu verlangsamen. Die Gesamtmenge an JavaScript, die für die Hydratisierung der Seite erforderlich war, blieb jedoch in etwa gleich. Das erklärt, warum sich die Leistung beim Seitenaufbau durch die reduzierte Codemenge nicht unbedingt verbessert hat.

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

Größenreduzierung durch detaillierte Blöcke

Viele Frameworks, darunter Next.js, verwenden clientseitiges Routing (über JavaScript), um für jeden Routenübergang neuere Script-Tags einzufügen. Aber wie werden diese dynamischen Chunks zur Build-Zeit vorbestimmt?

Next.js verwendet eine serverseitige Build-Manifestdatei, um zu bestimmen, 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 freigegebener Chunks in einer Next.js-Anwendung.

Diese neuere granulare Chunking-Strategie wurde zuerst in Next.js hinter einem Flag eingeführt und dort von einer Reihe von ersten Nutzern getestet. Viele konnten die Gesamtmenge an JavaScript, die für ihre gesamte Website verwendet wird, erheblich reduzieren:

Website JS-Änderung insgesamt 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 – über 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 verwendet, um gemeinsame Module zu definieren:

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 übernehmen, stellten sie auch bei vielen großen Websites erhebliche JavaScript-Reduzierungen fest:

Website JS-Änderung insgesamt 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 – über alle Routen (komprimiert)

Sehen Sie sich den PR an, um zu erfahren, wie diese Logik in die Webpack-Konfiguration implementiert wurde, die standardmäßig in Version 2.20.7 enthalten ist.

Fazit

Das Konzept, granulare Blöcke zu versenden, ist nicht auf Next.js, Gatsby oder webpack beschränkt. Jeder sollte die Chunking-Strategie seiner Anwendung verbessern, wenn sie einen großen „Commons“-Bundle-Ansatz verfolgt, unabhängig vom verwendeten Framework oder Modul-Bundler.

  • Wenn Sie dieselben Chunking-Optimierungen auf eine Vanilla-React-Anwendung anwenden möchten, sehen Sie sich diese React-Beispiel-App an. Sie verwendet eine vereinfachte Version der granularen Chunking-Strategie und kann Ihnen helfen, dieselbe Logik auf Ihre Website anzuwenden.
  • Für Rollup werden Chunks standardmäßig detailliert erstellt. Wenn Sie das Verhalten manuell konfigurieren möchten, lesen Sie manualChunks.