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

Una più recente strategia di chunking del webpack in Next.js e Gatsby riduce al minimo il codice duplicato per migliorare le prestazioni di caricamento delle pagine.

Chrome sta collaborando con strumenti e nell'ecosistema open source JavaScript. Di recente sono state eseguite diverse ottimizzazioni più recenti aggiunto per migliorare le prestazioni di caricamento di Next.js Gatsby. Questo articolo illustra una strategia di chunking granulare migliorata che ora viene fornito per impostazione predefinita in entrambi i framework.

Introduzione

Come molti framework web, Next.js e Gatsby utilizzano webpack come principale bundler. introdotto webpack v3 CommonsChunkPlugin per consentire di moduli di output condivisi tra diversi punti di ingresso in uno (o pochi) "comuni" un chunk (o chunking). Il codice condiviso può essere scaricato separatamente e memorizzato inizialmente nella cache del browser. migliorano le prestazioni di caricamento.

Questo modello è diventato popolare con molti framework di applicazioni a pagina singola che adottavano un punto di ingresso e configurazione del bundle simile alla seguente:

Configurazione comune per entry point e bundle

Sebbene pratico, il concetto di raggruppare tutto il codice dei moduli condivisi in un unico blocco ha la sua limitazioni. I moduli non condivisi in tutti i punti di ingresso possono essere scaricati per i percorsi che non li utilizzano con conseguente download di più codice del necessario. Ad esempio, quando viene caricato page1 il blocco common, carica il codice per moduleC anche se page1 non utilizza moduleC. Per questo motivo, insieme ad alcuni altri, webpack v4 ha rimosso il plug-in in favore di un nuovo Uno: SplitChunksPlugin.

Chunking migliorato

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

Tuttavia, molti framework web che utilizzano questo plug-in seguono ancora un "singolo-comune". di chunking la suddivisione. Next.js, ad esempio, genera un bundle commons contenente qualsiasi modulo utilizzata 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 memorizzati nella cache per qualsiasi punto di ingresso, l'euristica basata sull'utilizzo di includere moduli comuni usati in più di metà delle pagine non è molto efficace. La modifica di questo rapporto può produrre solo uno dei due risultati seguenti:

  • Se riduci le proporzioni, verrà scaricato un codice superfluo non necessario.
  • Se aumenti il rapporto, più codice viene duplicato su più route.

Per risolvere il problema, Next.js ha adottato una diversa configurazione perSplitChunksPlugin che riduce codice non necessario per qualsiasi route.

  • Qualsiasi modulo di terze parti sufficientemente grande (superiore a 160 kB) viene suddiviso in un blocco
  • Viene creato un blocco frameworks separato per le dipendenze del framework (react, react-dom e e così via)
  • Vengono creati tutti i blocchi condivisi necessari (fino a 25)
  • La dimensione minima da generare per un blocco viene modificata in 20 kB

Questa strategia di chunking granulare offre i seguenti vantaggi:

  • I tempi di caricamento delle pagine sono stati migliorati. L'emissione di più chunk condivisi, invece di uno solo, riduce al minimo la quantità di codice non necessario (o duplicato) per qualsiasi punto di ingresso.
  • Memorizzazione nella cache migliorata durante le navigazioni. Suddivisione di librerie di grandi dimensioni e dipendenze del framework in blocchi separati riduce la possibilità di invalidazione della cache poiché è improbabile che modifiche finché non viene eseguito un upgrade.

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

Altre richieste HTTP

SplitChunksPlugin ha definito la base per il chunking granulare e applicando questo approccio a una come Next.js, non era un concetto del tutto nuovo. Molti framework, tuttavia, hanno continuato a usare un'unica euristica e uno "comune" di strategia di offerta per diversi motivi. È incluso il dubbio che un numero maggiore di richieste HTTP può influire negativamente sulle prestazioni del sito.

I browser possono aprire solo un numero limitato di connessioni TCP a un'unica origine (6 per Chrome), quindi la riduzione al minimo del numero di blocchi restituiti da un bundler può garantire che il numero totale di richieste rimane al di sotto di questa soglia. Tuttavia, questo vale solo per HTTP/1.1. multiplexing in HTTP/2 consente lo streaming di più richieste in parallelo utilizzando una singola connessione su un origine dati. In altre parole, in genere non dobbiamo preoccuparci di limitare il numero di blocchi emessi dal nostro bundler.

Tutti i principali browser supportano HTTP/2. I team di Chrome e Next.js Volevo verificare se un numero maggiore di richieste fosse aumentato dividendo il singolo "commons" di Next.js. gruppo in più blocchi condivisi potrebbe influire in alcun modo sulle prestazioni di caricamento. Ha iniziato misurando il rendimento di un singolo sito modificando il numero massimo di richieste parallele utilizzando maxInitialRequests proprietà.

Prestazioni di caricamento pagina con un maggior numero di richieste

In una media di tre esecuzioni di più prove su una singola pagina web, load, start-render e le volte di First Contentful Paint sono rimaste quasi uguali quando si variava il valore iniziale massimo numero di richieste (da 5 a 15). È interessante notare che abbiamo notato solo un leggero overhead delle prestazioni dopo la suddivisione aggressiva in centinaia di richieste.

Prestazioni di caricamento delle pagine con centinaia di richieste

Ciò ha dimostrato che rimanere sotto 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 il conteggio di maxInitialRequest.

La modifica del numero massimo di richieste che si verificano in parallelo ha generato più di una singola richiesta un bundle condiviso, separandoli in modo appropriato per ciascun punto di ingresso una 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 ce ne fossero un effetto negativo sulle prestazioni di caricamento pagina. I risultati suggeriscono che l'impostazione di maxInitialRequests su 25 sulla pagina di test è stato ottimale perché ha ridotto le dimensioni del payload JavaScript senza rallentamenti in tutta la pagina. È rimasta la quantità totale di JavaScript necessaria per eseguire l'hydration della pagina più o meno la stessa cosa, il che spiega perché il rendimento del caricamento delle pagine non è necessariamente migliorato con la riduzione una certa quantità di codice.

webpack usa 30 kB come dimensione minima predefinita per la generazione di un blocco. Tuttavia, l'accoppiamento di Un valore di maxInitialRequests pari a 25 con una dimensione minima di 20 kB ha migliorato la memorizzazione nella cache.

Riduzioni delle dimensioni con blocchi granulari

Molti framework, incluso Next.js, si basano sul routing lato client (gestito da JavaScript) per inserire nuovi tag di script per ogni transizione di percorso. Ma come fanno a predeterminare questi blocchi dinamici al momento della creazione?

Next.js utilizza un file manifest della build lato server per determinare quali blocchi di output vengono utilizzati da diversi punti di ingresso. Per fornire queste informazioni anche al client, una versione ridotta del lato client è stato creato un file manifest di compilazione 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}`)) || []
  )
}
Output di più blocchi condivisi in un'applicazione Next.js.

Questa nuova strategia di chunking granulare è stata implementata per la prima volta in Next.js dietro un flag, in cui è stata testata su un di early adopter. Molti hanno riscontrato riduzioni significative del totale di JavaScript utilizzato per la propria 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 fornita per impostazione predefinita con la versione 9.2.

Gatsby

Gatsby seguiva lo stesso approccio di un modello di attribuzione euristica per la definizione di 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 Webpack per adottare una strategia di chunking granulare simile, hanno anche notato notevoli riduzioni di 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%
Riduzioni delle dimensioni di JavaScript - in tutte le route (compresso)

Dai un'occhiata al PR per capire come ha implementato questa logica nella configurazione webpack, 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. Per tutti dovrebbe prendere in considerazione di migliorare la strategia di chunking della propria applicazione se segue una grande varietà di "comune" approccio in bundle, indipendentemente dal framework o dal bundler di moduli utilizzato.

  • Se vuoi vedere le stesse ottimizzazioni di chunking applicate a un'applicazione Vanilla React, dai un'occhiata a questo esempio di reazione Google Cloud. Utilizza un una versione semplificata della strategia di chunking granulare e può aiutarti ad applicare tipo di logica al tuo sito.
  • Per l'aggregazione, i blocchi vengono creati in modo granulare per impostazione predefinita. Consulta questi dati: manualChunks per eseguire manualmente per configurare il comportamento.