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

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

Chrome arbeitet mit Tools und Frameworks in der Open-Source-Umgebung von JavaScript. Kürzlich wurden einige neuere Optimierungen vorgenommen, um die Ladeleistung von Next.js und Gatsby zu verbessern. In diesem Artikel wird eine verbesserte, detaillierte Aufteilungsstrategie beschrieben, die jetzt standardmäßig in beiden Frameworks verfügbar ist.

Einleitung

Wie viele Web-Frameworks verwenden Next.js und Gatsby webpack als Core Bundler. In Webpack Version 3 wurde CommonsChunkPlugin eingeführt, um Module, die von verschiedenen Einstiegspunkten gemeinsam verwendet werden, in einem (oder wenigen) „Commons“-Chunk (oder Blöcken) 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 mit einem Einstiegspunkt und einer Bundle-Konfiguration so beliebt:

Allgemeiner Einstiegspunkt und Bundle-Konfiguration

Das Konzept, den gesamten Code von freigegebenen Modulen zu einem einzigen Block zu bündeln, ist zwar praktikabel, hat aber auch Einschränkungen. Module, die nicht in jedem Einstiegspunkt freigegeben sind, können für Routen heruntergeladen werden, die sie nicht verwenden. Dadurch wird mehr Code heruntergeladen als nötig. Wenn page1 beispielsweise den common-Chunk lädt, wird der Code für moduleC geladen, obwohl page1 nicht moduleC verwendet. Aus diesem und einigen anderen Gründen wurde das Plug-in von Webpack Version 4 entfernt und durch ein neues ersetzt: SplitChunksPlugin.

Verbessertes Chunking

Die Standardeinstellungen für SplitChunksPlugin eignen sich für die meisten Nutzer. In Abhängigkeit von einer Reihe von conditions 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 Blöcken. Next.js würde beispielsweise ein commons-Bundle generieren, das jedes Modul enthält, das auf mehr als 50% der Seiten verwendet wird, und 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)[\\/]/,
      },
    },
  },

Das Einbinden von Framework-abhängigem Code in einen gemeinsamen Block bedeutet, dass er für jeden Einstiegspunkt heruntergeladen und im Cache gespeichert werden kann. Die nutzungsbasierte Heuristik bei der Aufnahme gängiger Module, die in mehr als der Hälfte der Seiten verwendet 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 reduzieren, wird mehr unnötiger Code heruntergeladen.
  • Wenn Sie das Verhältnis erhöhen, wird mehr Code auf mehreren Routen dupliziert.

Um dieses Problem zu lösen, verwendete Next.js eine andere Konfiguration für SplitChunksPlugin, mit der unnötigen Code für jede Route reduziert wird.

  • Jedes ausreichend große Drittanbietermodul (größer als 160 KB) wird in einen eigenen Block 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 Block wird in 20 KB geändert.

Diese detaillierte Strategie für die Aufschlüsselung bietet folgende Vorteile:

  • Die Seitenladezeiten werden verkürzt. Wenn Sie mehrere freigegebene Blöcke anstelle eines einzelnen ausgeben, wird die Menge an nicht benötigtem (oder doppeltem) Code für jeden Einstiegspunkt minimiert.
  • Verbessertes Caching während der Navigation Das Aufteilen großer Bibliotheken und Framework-Abhängigkeiten in separate Blöcke reduziert die Wahrscheinlichkeit einer Cache-Entwertung, da sich beide bis zum Upgrade wahrscheinlich nicht ändern werden.

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

Mehr HTTP-Anfragen

SplitChunksPlugin definiert die Grundlage für eine detaillierte Aufteilung. Dieser Ansatz auf ein Framework wie Next.js war kein völlig neues Konzept. In vielen Frameworks wurde jedoch aus einigen Gründen weiterhin eine einzelne Heuristik und eine „Commons“-Bündelstrategie verwendet. Dazu gehört auch die Befürchtung, dass sich viel mehr 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 also die Anzahl der von einem Bundler ausgegebenen Blöcke minimieren, kann die Gesamtzahl der Anfragen unter diesem Grenzwert bleiben. Dies gilt jedoch nur für HTTP/1.1. Mit Multiplexing in HTTP/2 können mehrere Anfragen über eine einzelne Verbindung und einen einzigen Ursprung parallel gestreamt werden. Mit anderen Worten: Wir müssen uns im Allgemeinen keine Gedanken über die Begrenzung der Anzahl der von unserem Bundler ausgegebenen Blöcke machen.

Alle gängigen Browser unterstützen HTTP/2. Die Teams von Chrome und Next.js wollten herausfinden, ob eine Erhöhung der Anzahl der Anfragen durch Aufteilen des einzelnen „Commons“-Bundles von Next.js in mehrere freigegebene Blöcke die Ladeleistung beeinträchtigt hat. Zuerst wurde die Leistung einer einzelnen Website gemessen und die maximale Anzahl paralleler Anfragen mithilfe des Attributs maxInitialRequests geändert.

Leistung beim Seitenaufbau bei erhöhter Anzahl von Anfragen

Bei durchschnittlich drei Ausführungen mehrerer Tests auf einer einzelnen Webseite blieben die Zeiten für load, Start-Rendering und First Contentful Paint ungefähr gleich, wenn die maximale Anzahl der anfänglichen Anfragen (von 5 auf 15) variiert wurde. Interessanterweise stellten wir erst nach der aggressiven Aufteilung auf Hunderte von Anfragen einen geringfügigen Leistungsaufwand fest.

Leistung beim Seitenaufbau mit Hunderten von Anfragen

Dies zeigte, dass unter einem zuverlässigen Grenzwert (20–25 Anfragen) das richtige Gleichgewicht zwischen Ladeleistung und Caching-Effizienz gelegt wurde. Nach einigen Referenztests wurden 25 als Anzahl für maxInitialRequest ausgewählt.

Das Ändern der maximalen Anzahl paralleler Anfragen führte zu mehr als einem freigegebenen Bundle und durch eine entsprechende Trennung für jeden Einstiegspunkt wurde die Menge an nicht benötigtem Code für dieselbe Seite erheblich reduziert.

Reduzierung der JavaScript-Nutzlast durch verstärktes Aufteilen

Bei diesem Test ging es nur darum, die Anzahl der Anfragen zu ändern, um festzustellen, ob sie negative Auswirkungen auf die Ladeleistung der Seite haben. 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, das zum Hydisieren der Seite erforderlich war, blieb ungefähr gleich. Das erklärt, warum sich die Leistung beim Laden der Seite mit der reduzierten Codemenge nicht unbedingt verbessert hat.

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

Größenreduzierungen mit detaillierten Blöcken

Viele Frameworks, einschließlich Next.js, basieren auf dem clientseitigen Routing, das von JavaScript verarbeitet wird, um neuere Script-Tags für jeden Routenwechsel einzufügen. Aber wie werden diese dynamischen Blöcke bei der Erstellung vorab festgelegt?

Next.js verwendet eine serverseitige Build-Manifestdatei, um zu bestimmen, welche ausgegebenen Blöcke von verschiedenen Einstiegspunkten verwendet werden. Um diese Informationen auch dem Client bereitzustellen, 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 Blöcke in einer Next.js-Anwendung.

Diese neuere detaillierte Aufteilungsstrategie wurde zuerst in Next.js hinter einem Flag eingeführt, wo sie mit einer Reihe von ersten Nutzern getestet wurde. Viele Unternehmen verzeichneten deutlich weniger JavaScript-Elemente, die auf ihrer gesamten Website verwendet wurden:

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%
JavaScript-Größenreduzierungen – über alle Routen (komprimiert)

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

Gatsby

Gatsby verfolgte den gleichen Ansatz, bei dem zum Definieren allgemeiner Module eine nutzungsbasierte Heuristik verwendet wurde:

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 im Hinblick auf eine ähnliche, detaillierte Chunking-Strategie konnte das Unternehmen auch erhebliche JavaScript-Einsparungen bei vielen großen Websites feststellen:

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 %
JavaScript-Größenreduzierungen – über alle Routen (komprimiert)

Im PR wird beschrieben, wie diese Logik in die Webpack-Konfiguration implementiert wurde, die standardmäßig in Version 2.20.7 enthalten ist.

Fazit

Das Konzept des Versand detaillierter Blöcke ist nicht spezifisch für Next.js, Gatsby oder gar Webpack. Unabhängig vom verwendeten Framework oder Modul-Bundler sollte jeder erwägen, die Aufteilungsstrategie der Anwendung zu verbessern, wenn sie einem großen „Commons“-Ansatz folgt.

  • Wenn Sie dieselben Optimierungsmöglichkeiten für die Aufteilung auf eine einfache React-Anwendung anwenden möchten, sehen Sie sich diese React-Beispielanwendung an. Sie verwendet eine vereinfachte Version der detaillierten Strategie für die Aufteilung und kann Ihnen helfen, dieselbe Logik auf Ihre Website anzuwenden.
  • Für Rollups werden Blöcke standardmäßig detailliert erstellt. Über manualChunks erfahren Sie, wie Sie das Verhalten manuell konfigurieren können.