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 JavaScript-Open-Source-System 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 Kern-Bundler. Mit webpack v3 wurde CommonsChunkPlugin
eingeführt, um Module, die zwischen verschiedenen Einstiegspunkten gemeinsam genutzt werden, in einem einzigen oder wenigen „Commons“-Chunks (oder -Chunks) auszugeben. 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:
Das Bündeln des gesamten gemeinsamen Modulcodes in einem einzigen Chunk ist zwar praktisch, hat aber auch seine Grenzen. Module, die nicht an allen Einstiegspunkten freigegeben werden, können für Routen heruntergeladen werden, in denen sie nicht verwendet werden. Dies führt dazu, dass mehr Code als nötig heruntergeladen wird. Wenn page1
beispielsweise den common
-Chunk lädt, wird auch der Code für moduleC
geladen, obwohl page1
moduleC
nicht verwendet.
Aus diesem Grund wurde das Plugin in Webpack v4 zusammen mit einigen anderen entfernt und durch ein neues ersetzt: SplitChunksPlugin
.
Verbessertes Chunking
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, folgen jedoch weiterhin dem Ansatz „Single Commons“ für die Chunk-Aufteilung. 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 häufig verwendete Module in mehr als der Hälfte der Seiten eingefügt werden, ist jedoch 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.
Um dieses Problem zu lösen, hat Next.js eine andere Konfiguration fürSplitChunksPlugin
eingeführt, 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 Chunks erstellt, wie erforderlich (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 Befürchtung, dass viele weitere HTTP-Anfragen die Websiteleistung beeinträchtigen können.
Browser können nur eine begrenzte Anzahl von TCP-Verbindungen zu einer einzelnen Quelle öffnen (6 für Chrome). Wenn Sie also die Anzahl der von einem Bundler ausgegebene Chunks minimieren, kann die Gesamtzahl der Anfragen unter diesem Grenzwert bleiben. Dies gilt jedoch nur für HTTP/1.1. Durch Multiplexing in HTTP/2 können mehrere Anfragen parallel über eine einzige Verbindung über einen einzigen Ursprung gestreamt werden. Mit anderen Worten: Wir müssen uns in der Regel keine Gedanken über die Begrenzung der Anzahl der von unserem Bundler gesendeten Chunks machen.
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.
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.
Das Ergebnis zeigte, dass ein zuverlässiger Grenzwert (20 bis 25 Anfragen) das richtige Gleichgewicht zwischen Ladeleistung und Caching-Effizienz bietet. Nach einigen Vorabtests wurde 25 als Anzahl der maxInitialRequest
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 konnte die Menge an unnötigem Code für dieselbe Seite erheblich reduziert werden.
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 die JavaScript-Nutzlastgröße 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.
In webpack wird 30 KB als Standardmindestgröße für einen zu generierenden Block verwendet. Die Kombination eines maxInitialRequests
-Werts von 25 mit einer Mindestgröße von 20 KB führte jedoch zu einem besseren Caching.
Größere Einsparungen durch detailliertere Datenblöcke
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}`)) || []
)
}
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-Änderung insgesamt | Unterschied in % |
---|---|---|
https://www.barnebys.com/ | –238 KB | -23% |
https://sumup.com/ | – 220 KB | -30% |
https://www.hashicorp.com/ | – 11 MB | –71% |
Die finale Version wurde standardmäßig in Version 9.2 ausgeliefert.
Gatsby
Gatsby hat früher denselben Ansatz verfolgt, bei dem eine nutzungsbasierte Heuristik zur Definition gängiger Module 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 für eine ähnliche detaillierte Chunking-Strategie konnte das Unternehmen auch bei vielen großen Websites erhebliche JavaScript-Einsparungen erzielen:
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 % |
Im PR sehen Sie, wie diese Logik in die Webpack-Konfiguration implementiert wurde, die in Version 2.20.7 standardmäßig enthalten ist.
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 standardmäßig detaillierte Chunks erstellt. Unter
manualChunks
können Sie das Verhalten manuell konfigurieren.