Una strategia di suddivisione dei chunk webpack più recente in Next.js e Gatsby riduce al minimo il codice duplicato per migliorare le prestazioni di caricamento della pagina.
Chrome collabora con strumenti e framework nell'ecosistema open source JavaScript. Di recente sono state aggiunte diverse ottimizzazioni più recenti per migliorare le prestazioni di caricamento di Next.js e Gatsby. Questo articolo descrive una strategia di suddivisione granulare migliorata che ora viene fornita per impostazione predefinita in entrambi i framework.
Introduzione
Come molti framework web, Next.js e Gatsby utilizzano webpack come bundler principale. webpack v3 ha introdotto CommonsChunkPlugin
per consentire l'output di moduli condivisi tra diversi punti di ingresso in un unico (o pochi) chunk "commons" (o chunk). Il codice condiviso può essere scaricato separatamente e memorizzato nella cache del browser in anticipo, il che può
migliorare le prestazioni di caricamento.
Questo pattern è diventato popolare con molti framework di applicazioni a pagina singola che adottano un punto di ingresso e una configurazione del bundle simile a questa:
Sebbene pratico, il concetto di raggruppare tutto il codice del modulo condiviso in un unico chunk presenta dei
limiti. I moduli non condivisi in ogni punto di ingresso possono essere scaricati per le route che non li utilizzano, con conseguente download di più codice del necessario. Ad esempio, quando viene caricato il blocco page1
, viene caricato il codice per moduleC
anche se page1
non utilizza moduleC
.common
Per questo motivo, oltre ad altri, webpack v4 ha rimosso il plug-in a favore di uno nuovo: SplitChunksPlugin
.
Chunking migliorato
Le impostazioni predefinite per SplitChunksPlugin
sono adatte alla maggior parte degli utenti. Vengono creati più blocchi suddivisi
a seconda di una serie di condizioni
per evitare il recupero di codice duplicato su più route.
Tuttavia, molti framework web che utilizzano questo plug-in seguono ancora un approccio "single-commons" alla suddivisione dei chunk. Next.js, ad esempio, genererebbe un bundle commons
contenente qualsiasi modulo utilizzato in più del 50% delle pagine e tutte le dipendenze del framework (react
, react-dom
e così via).
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)[\\/]/,
},
},
},
Sebbene l'inclusione di codice dipendente dal framework in un chunk condiviso significhi che può essere scaricato e memorizzato nella cache per qualsiasi punto di ingresso, l'euristica basata sull'utilizzo che include moduli comuni utilizzati in più della metà delle pagine non è molto efficace. La modifica di questo rapporto comporterebbe solo uno dei due risultati:
- Se riduci il rapporto, viene scaricato codice non necessario.
- Se aumenti il rapporto, più codice viene duplicato su più percorsi.
Per risolvere questo problema, Next.js ha adottato una configurazione
diversa perSplitChunksPlugin
che riduce
il codice non necessario per qualsiasi route.
- Qualsiasi modulo di terze parti sufficientemente grande (superiore a 160 KB) viene suddiviso in un proprio chunk individuale
- Viene creato un blocco
frameworks
separato per le dipendenze del framework (react
,react-dom
e così via). - Vengono creati tutti i blocchi condivisi necessari (fino a 25).
- La dimensione minima di un segmento da generare è stata modificata a 20 kB
Questa strategia di suddivisione granulare offre i seguenti vantaggi:
- I tempi di caricamento delle pagine sono migliorati. L'emissione di più blocchi condivisi, anziché di uno solo, riduce al minimo la quantità di codice non necessario (o duplicato) per qualsiasi punto di ingresso.
- Miglioramento della memorizzazione nella cache durante la navigazione. La suddivisione di librerie e dipendenze del framework di grandi dimensioni in blocchi separati riduce la possibilità di invalidare la cache, poiché è improbabile che entrambi cambino fino a quando non viene eseguito un upgrade.
Puoi visualizzare l'intera configurazione adottata da Next.js in webpack-config.ts
.
Altre richieste HTTP
SplitChunksPlugin
ha definito la base per la suddivisione granulare e l'applicazione di questo approccio a un framework come Next.js non era un concetto del tutto nuovo. Tuttavia, molti framework hanno continuato a utilizzare
una singola strategia di bundle euristica e "commons" per diversi motivi. Ciò include la preoccupazione che
molte più richieste HTTP possano influire negativamente sulle prestazioni del sito.
I browser possono aprire solo un numero limitato di connessioni TCP a una singola origine (6 per Chrome), quindi ridurre al minimo il numero di blocchi generati da un bundler può garantire che il numero totale di richieste rimanga al di sotto di questa soglia. Tuttavia, questo vale solo per HTTP/1.1. Il multiplexing in HTTP/2 consente di trasmettere in streaming più richieste in parallelo utilizzando una singola connessione su una singola origine. In altre parole, in genere non dobbiamo preoccuparci di limitare il numero di blocchi emessi dal bundler.
Tutti i principali browser supportano HTTP/2. I team di Chrome e Next.js
volevano verificare se l'aumento del numero di richieste dividendo il singolo bundle "commons" di Next.js
in più blocchi condivisi avrebbe influito in qualche modo sul rendimento di caricamento. Hanno iniziato misurando il rendimento di un singolo sito modificando il numero massimo di richieste parallele utilizzando la proprietà
maxInitialRequests
.
In una media di tre esecuzioni di più prove su una singola pagina web, i tempi di
load
,
start-render
e First Contentful Paint sono rimasti pressoché invariati quando è stato variato il conteggio massimo delle richieste
iniziali (da 5 a 15). È interessante notare che abbiamo notato un leggero sovraccarico delle prestazioni solo
dopo aver suddiviso in modo aggressivo centinaia di richieste.
Ciò ha dimostrato che rimanere al di sotto di una soglia affidabile (20-25 richieste) ha trovato il giusto equilibrio
tra le prestazioni di caricamento e l'efficienza della memorizzazione nella cache. Dopo alcuni test di base, è stato selezionato 25 come
conteggio maxInitialRequest
.
La modifica del numero massimo di richieste parallele ha comportato la creazione di più di un singolo bundle condiviso e la loro separazione appropriata per ogni punto di ingresso ha ridotto significativamente la quantità di codice non necessario per la stessa pagina.
Questo esperimento riguardava solo la modifica del numero di richieste per verificare se ci sarebbero stati
effetti negativi sulle prestazioni di caricamento della pagina. I risultati suggeriscono che l'impostazione di maxInitialRequests
su
25
nella pagina di test era ottimale perché riduceva le dimensioni del payload JavaScript senza rallentare
la pagina. La quantità totale di JavaScript necessaria per l'idratazione della pagina è rimasta
più o meno la stessa, il che spiega perché il rendimento del caricamento della pagina non è migliorato necessariamente con la riduzione
della quantità di codice.
webpack utilizza 30 KB come dimensione minima predefinita per la generazione di un chunk. Tuttavia, l'accoppiamento di un valore
maxInitialRequests
di 25 con una dimensione minima di 20 KB ha portato a una memorizzazione nella cache migliore.
Riduzioni delle dimensioni con blocchi granulari
Molti framework, tra cui Next.js, si basano sul routing lato client (gestito da JavaScript) per inserire tag script più recenti per ogni transizione di route. Ma come fanno a predeterminare questi blocchi dinamici al momento della creazione?
Next.js utilizza un file manifest di build lato server per determinare quali blocchi di output vengono utilizzati da diversi punti di ingresso. Per fornire queste informazioni anche al client, è stato creato un file manifest di build lato client abbreviato per mappare tutte le dipendenze per ogni punto di ingresso.
// 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}`)) || []
)
}

Questa nuova strategia di suddivisione granulare è stata implementata per la prima volta in Next.js dietro un flag, dove è stata testata su un numero di primi utenti. Molti hanno riscontrato riduzioni significative del codice JavaScript totale utilizzato per l'intero sito:
Sito web | Variazione totale JS | % di differenza |
---|---|---|
https://www.barnebys.com/ | -238 KB | -23% |
https://sumup.com/ | -220 kB | -30% |
https://www.hashicorp.com/ | -11 MB | -71% |
La versione finale è stata spedita per impostazione predefinita nella versione 9.2.
Gatsby
Gatsby utilizzava lo stesso approccio di un'euristica basata sull'utilizzo per definire i moduli comuni:
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)[\\/]/,
},
Ottimizzando la configurazione di webpack per adottare una strategia di suddivisione granulare simile, hanno anche notato riduzioni considerevoli di JavaScript in molti siti di grandi dimensioni:
Sito web | Variazione totale JS | % di differenza |
---|---|---|
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% |
Dai un'occhiata alla PR per capire come hanno implementato questa logica nella configurazione di webpack, fornita per impostazione predefinita nella versione 2.20.7.
Conclusione
Il concetto di spedizione di blocchi granulari non è specifico di Next.js, Gatsby o webpack. Tutti dovrebbero prendere in considerazione la possibilità di migliorare la strategia di suddivisione in blocchi dell'applicazione se segue un approccio di tipo "commons" di grandi dimensioni, indipendentemente dal framework o dal bundler di moduli utilizzato.
- Se vuoi vedere le stesse ottimizzazioni di suddivisione applicate a un'applicazione React di base, dai un'occhiata a questa app React di esempio. Utilizza una versione semplificata della strategia di suddivisione granulare e può aiutarti a iniziare ad applicare lo stesso tipo di logica al tuo sito.
- Per il rollup, i chunk vengono creati in modo granulare per impostazione predefinita. Consulta
manualChunks
se vuoi configurare manualmente il comportamento.