Prestazioni migliorate per il caricamento delle pagine di Next.js e Gatsby con suddivisione granulare

Una strategia di suddivisione in blocchi 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 gli strumenti e i framework dell'ecosistema open source di JavaScript. Di recente sono state aggiunte diverse ottimizzazioni più recenti per migliorare le prestazioni di caricamento di Next.js e Gatsby. Questo articolo illustra una strategia di chunking dettagliata migliorata che ora viene fornita per impostazione predefinita in entrambi i framework.

Introduzione

Come molti framework web, Next.js e Gatsby utilizzano webpack come core bundler. webpack v3 ha introdotto CommonsChunkPlugin per consentire di produrre moduli condivisi tra diversi punti di ingresso in uno (o pochi) blocchi "comuni". Il codice condiviso può essere scaricato separatamente e archiviato nella cache del browser in anticipo, il che può migliorare le prestazioni in fase di caricamento.

Questo pattern è diventato popolare con molti framework di applicazioni a pagina singola che adottano un punto di contatto e una configurazione del bundle simile alla seguente:

Configurazione comune di bundle e punto di contatto

Sebbene pratico, il concetto di raggruppare tutto il codice dei moduli condivisi in un unico blocco ha le sue limitazioni. I moduli non condivisi in ogni punto di contatto possono essere scaricati per i percorsi che non li utilizzano, con il risultato che viene scaricato più codice del necessario. Ad esempio, quando page1 carica il chunk common, carica il codice per moduleC anche se page1 non utilizza moduleC. Per questo motivo, insieme ad altri, webpack 4 ha rimosso il plug-in in favore di un nuovo plug-in: SplitChunksPlugin.

Suddivisione in blocchi migliorata

Le impostazioni predefinite per SplitChunksPlugin funzionano bene per la maggior parte degli utenti. Vengono creati più chunk suddivisi in base a una serie di condizioni per impedire il recupero di codice duplicato su più percorsi.

Tuttavia, molti framework web che utilizzano questo plug-in seguono ancora un approccio "single-commons" alla suddivisione in chunk. Next.js, ad esempio, genera un bundle commons contenente qualsiasi modulo utilizzato in più del 50% delle pagine e in 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)[\\/]/,
      },
    },
  },

Anche se includere codice dipendente dal framework in un blocco condiviso significa che può essere scaricato e memorizzato nella cache per qualsiasi punto di ingresso, l'euristica basata sull'utilizzo di includere moduli comuni utilizzati in più di metà delle pagine non è molto efficace. La modifica di questo rapporto può avere solo due risultati:

  • Se riduci le proporzioni, verrà scaricato un codice aggiuntivo non necessario.
  • Se aumenti il rapporto, viene duplicato più codice in 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 singoli chunk
  • Viene creato un blocco frameworks separato per le dipendenze del framework (react, react-dom e così via)
  • Vengono creati tutti i chunk condivisi necessari (fino a 25)
  • La dimensione minima per un chunk da generare è stata modificata in 20 KB

Questa strategia di chunking granulare offre i seguenti vantaggi:

  • I tempi di caricamento delle pagine sono migliorati. L'emissione di più chunk condivisi, anziché di uno solo, consente di minimizzare la quantità di codice non necessario (o duplicato) per qualsiasi punto di contatto.
  • Memorizzazione nella cache migliorata durante le navigazioni. La suddivisione di librerie di grandi dimensioni e dipendenze del framework in blocchi separati riduce la possibilità di invalidazione della cache, poiché è improbabile che entrambe le parti cambino fino a quando non viene eseguito un upgrade.

Puoi vedere l'intera configurazione adottata da Next.js in webpack-config.ts.

Più richieste HTTP

SplitChunksPlugin ha definito la base per un chunking 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 basata su regole euristiche e su pacchetti "comuni" per diversi motivi. Ciò include il timore che molte altre 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 minimizzare il numero di chunk generati da un aggregatore 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 lo streaming di 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 chunk emessi dal nostro 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 sulle prestazioni del caricamento. Hanno iniziato misurando il rendimento di un singolo sito modificando il numero massimo di richieste parallele utilizzando la proprietà maxInitialRequests.

Prestazioni di caricamento pagina con un maggior numero di richieste

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 se si variava il numero massimo di richieste iniziali (da 5 a 15). È interessante notare che abbiamo notato un lieve overhead del rendimento solo dopo la suddivisione aggressiva in centinaia di richieste.

Prestazioni di caricamento delle pagine con centinaia di richieste

Ciò ha dimostrato che rimanere al di sotto di una soglia affidabile (20-25 richieste) ha consentito di trovare il giusto equilibrio tra prestazioni di caricamento ed efficienza della cache. Dopo alcuni test di riferimento, è stato selezionato il valore 25 come conteggio maxInitialRequest.

La modifica del numero massimo di richieste in parallelo ha generato più di un singolo bundle condiviso e la loro separazione in modo appropriato per ogni punto di contatto ha ridotto notevolmente la quantità di codice non necessario per la stessa pagina.

Riduzioni del payload JavaScript con un maggior chunking

Questo esperimento riguardava solo la modifica del numero di richieste per verificare se si verificasse un effetto negativo sul rendimento del caricamento della pagina. I risultati suggeriscono che l'impostazione di maxInitialRequests su 25 nella pagina di test è stata ottimale perché ha ridotto le dimensioni del payload JavaScript senza rallentare la pagina. La quantità totale di codice JavaScript necessaria per eseguire l'idratazione della pagina è rimasta invariata, il che spiega perché il rendimento del caricamento della pagina non è necessariamente migliorato con la riduzione della quantità di codice.

webpack utilizza 30 KB come dimensione minima predefinita per un chunk da generare. Tuttavia, l'accoppiamento di un valore maxInitialRequests pari a 25 con una dimensione minima di 20 KB ha comportato una migliore memorizzazione nella cache.

Riduzioni delle dimensioni con chunk granulari

Molti framework, tra cui Next.js, si basano sul routing lato client (gestito da JavaScript) per iniettare tag script più recenti per ogni transizione di route. Ma come vengono predeterminati questi chunk dinamici in fase di compilazione?

Next.js utilizza un file manifest di compilazione lato server per determinare quali chunk di output vengono utilizzati da diversi punti di contatto. Per fornire queste informazioni anche al client, è stato creato un file manifest di compilazione lato client abbreviato per mappare tutte le dipendenze per ogni punto di contatto.

// 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}`)) || []
  )
}
Output di più chunk condivisi in un'applicazione Next.js.

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 early adopter. Molti hanno registrato riduzioni significative del codice JavaScript totale utilizzato per l'intero sito:

Sito web Variazione JS totale % di differenza
https://www.barnebys.com/ -238 kB -23%
https://sumup.com/ - 220 kB 30%
https://www.hashicorp.com/ -11 MB 71%
Riduzioni delle dimensioni di JavaScript - in tutte le route (compresso)

La versione finale è stata inviata per impostazione predefinita nella versione 9.2.

Gatsby

Gatsby utilizzava lo stesso approccio di utilizzo 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, ha anche riscontrato riduzioni significative del codice JavaScript in molti siti di grandi dimensioni:

Sito web Variazione JS totale % 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%
Riduzione delle dimensioni di JavaScript in tutti i percorsi (compressi)

Dai un'occhiata al PR per capire come hanno implementato questa logica nella configurazione di webpack, che viene fornita per impostazione predefinita nella versione 2.20.7.

Conclusione

Il concetto di spedizione di blocchi granulari non è specifico per Next.js, Gatsby o persino webpack. Tutti dovrebbero valutare la possibilità di migliorare la strategia di suddivisione dell'applicazione se segue un approccio di aggregazione di elementi comuni 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 standard, consulta questa app React di esempio. Utilizza una versione semplificata della strategia di suddivisione granulare e può aiutarti ad applicare lo stesso tipo di logica al tuo sito.
  • Per l'aggregazione, i blocchi vengono creati in modo granulare per impostazione predefinita. Consulta manualChunks se vuoi configurare manualmente il comportamento.