Usare la memorizzazione nella cache a lungo termine

In che modo webpack contribuisce alla memorizzazione nella cache degli asset

L'operazione successiva (dopo aver ottimizzato le dimensioni dell'app che migliora il tempo di caricamento dell'app è la memorizzazione nella cache. usalo per mantenere parti dell'app sul client ed evitare di scaricarle di nuovo ogni volta.

Usa il controllo delle versioni e le intestazioni cache del bundle

L'approccio comune per la memorizzazione nella cache è:

  1. comunica al browser di memorizzare nella cache un file per molto tempo (ad esempio, un anno):

    # Server header
    Cache-Control: max-age=31536000
    

    Se non sai cosa fa Cache-Control, leggi l'eccellente post di Jake Archibald sulle best practice per la memorizzazione nella cache.

  2. e rinomina il file quando viene modificato per forzarne un nuovo download:

    <!-- Before the change -->
    <script src="./index-v15.js"></script>
    
    <!-- After the change -->
    <script src="./index-v16.js"></script>
    

Questo approccio indica al browser di scaricare il file JS, memorizzarlo nella cache e utilizzare la copia memorizzata nella cache. Il browser raggiungerà la rete solo se il nome del file cambia (o se è trascorso un anno).

Con il webpack si esegue la stessa operazione, ma invece di un numero di versione, specifichi l'hash del file. Per includere l'hash nel nome del file, utilizza [chunkhash]:

// webpack.config.js
module.exports = {
  entry: './index.js',
  output: {
    filename: 'bundle.[chunkhash].js' // → bundle.8e0d62a03.js
  }
};

Se hai bisogno del nome del file per inviarlo al client, utilizza HtmlWebpackPlugin o WebpackManifestPlugin.

Il HtmlWebpackPlugin è un approccio semplice, ma meno flessibile. Durante la compilazione, il plug-in genera un file HTML che include tutte le risorse compilate. Se la logica del tuo server non è complessa, allora dovrebbe essere sufficiente:

<!-- index.html -->
<!DOCTYPE html>
<!-- ... -->
<script src="bundle.8e0d62a03.js"></script>

WebpackManifestPlugin è un approccio più flessibile, utile se hai una parte complessa del server. Durante la build, genera un file JSON con una mappatura tra i nomi dei file senza hash e i nomi dei file con hash. Usate questo JSON sul server per scoprire con quale file lavorare:

// manifest.json
{
  "bundle.js": "bundle.8e0d62a03.js"
}

Per approfondire

Estrai le dipendenze e il runtime in un file separato

Dipendenze

Le dipendenze dell'app tendono a cambiare meno spesso del codice effettivo dell'app. Se li sposti in un file separato, il browser sarà in grado di memorizzarli nella cache separatamente e non li scaricherà nuovamente ogni volta che il codice dell'app viene modificato.

Per estrarre le dipendenze in un blocco separato, esegui tre passaggi:

  1. Sostituisci il nome del file di output con [name].[chunkname].js:

    // webpack.config.js
    module.exports = {
      output: {
        // Before
        filename: 'bundle.[chunkhash].js',
        // After
        filename: '[name].[chunkhash].js'
      }
    };
    

    Quando il webpack crea l'app, sostituisce [name] con il nome di un blocco. Se non aggiungiamo la parte [name], dovremo differenziare i blocchi in base al loro hash, il che è piuttosto difficile.

  2. Converti il campo entry in un oggetto:

    // webpack.config.js
    module.exports = {
      // Before
      entry: './index.js',
      // After
      entry: {
        main: './index.js'
      }
    };
    

    In questo snippet, "main" è il nome di un blocco. Questo nome verrà sostituito al posto di [name] nel passaggio 1.

    A questo punto, se crei l'app, questo blocco includerà l'intero codice dell'app, proprio come non abbiamo fatto con questi passaggi. Ma la situazione cambierà tra un secondo.

  3. In webpack 4, aggiungi l'opzione optimization.splitChunks.chunks: 'all' nella configurazione del webpack:

    // webpack.config.js (for webpack 4)
    module.exports = {
      optimization: {
        splitChunks: {
          chunks: 'all'
        }
      }
    };
    

    Questa opzione consente la suddivisione intelligente del codice. Con questo strumento, webpack estrae il codice del fornitore se supera i 30 kB (prima della minimizzazione e di gzip). Viene inoltre estratto il codice comune, utile se la tua build produce diversi bundle (ad esempio se suddividi l'app in route).

    Nel webpack 3, aggiungi CommonsChunkPlugin:

    // webpack.config.js (for webpack 3)
    module.exports = {
      plugins: [
        new webpack.optimize.CommonsChunkPlugin({
        // A name of the chunk that will include the dependencies.
        // This name is substituted in place of [name] from step 1
        name: 'vendor',
    
        // A function that determines which modules to include into this chunk
        minChunks: module => module.context && module.context.includes('node_modules'),
        })
      ]
    };
    

    Questo plug-in prende tutti i moduli che includono i percorsi node_modules e li sposta in un file separato chiamato vendor.[chunkhash].js.

Dopo queste modifiche, ogni build genererà due file invece di uno: main.[chunkhash].js e vendor.[chunkhash].js (vendors~main.[chunkhash].js per webpack 4). Nel caso del webpack 4, il bundle del fornitore potrebbe non essere generato se le dipendenze sono piccole. È possibile farlo:

$ webpack
Hash: ac01483e8fec1fa70676
Version: webpack 3.8.1
Time: 3816ms
                        Asset      Size  Chunks             Chunk Names
 ./main.00bab6fd3100008a42b0.js   82 kB       0  [emitted]  main
./vendor.d9e134771799ecdf9483.js  47 kB       1  [emitted]  vendor

Il browser memorizza questi file nella cache separatamente e scarica nuovamente solo il codice che cambia.

Codice runtime Webpack

Sfortunatamente, estrarre solo il codice del fornitore non è sufficiente. Se provi a modificare qualcosa nel codice dell'app:

// index.js
…
…

// E.g. add this:
console.log('Wat');

noterai che anche l'hash vendor cambia:

                           Asset   Size  Chunks             Chunk Names
./vendor.d9e134771799ecdf9483.js  47 kB       1  [emitted]  vendor

                            Asset   Size  Chunks             Chunk Names
./vendor.e6ea4504d61a1cc1c60b.js  47 kB       1  [emitted]  vendor

Questo accade perché il bundle webpack, a parte il codice dei moduli, ha un runtime, ovvero una piccola porzione di codice che gestisce l'esecuzione del modulo. Quando suddividi il codice in più file, questa porzione di codice inizia a includere una mappatura tra gli ID dei blocchi e i file corrispondenti:

// vendor.e6ea4504d61a1cc1c60b.js
script.src = __webpack_require__.p + chunkId + "." + {
    "0": "2f2269c7f0a55a5c1871"
}[chunkId] + ".js";

Webpack include questo runtime nell'ultimo blocco generato, che nel nostro caso è vendor. Ogni volta che un blocco cambia, cambia anche questa porzione di codice, causando la modifica dell'intero blocco vendor.

Per risolvere questo problema, spostiamo il runtime in un file separato. In webpack 4, puoi farlo abilitando l'opzione optimization.runtimeChunk:

// webpack.config.js (for webpack 4)
module.exports = {
  optimization: {
    runtimeChunk: true
  }
};

Nel webpack 3, crea un blocco vuoto aggiuntivo con CommonsChunkPlugin:

// webpack.config.js (for webpack 3)
module.exports = {
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks: module => module.context && module.context.includes('node_modules')
    }),
    // This plugin must come after the vendor one (because webpack
    // includes runtime into the last chunk)
    new webpack.optimize.CommonsChunkPlugin({
      name: 'runtime',
      // minChunks: Infinity means that no app modules
      // will be included into this chunk
      minChunks: Infinity
    })
  ]
};

Dopo queste modifiche, per ogni build verranno generati tre file:

$ webpack
Hash: ac01483e8fec1fa70676
Version: webpack 3.8.1
Time: 3816ms
                            Asset     Size  Chunks             Chunk Names
   ./main.00bab6fd3100008a42b0.js    82 kB       0  [emitted]  main
 ./vendor.26886caf15818fa82dfa.js    46 kB       1  [emitted]  vendor
./runtime.79f17c27b335abc7aaf4.js  1.45 kB       3  [emitted]  runtime

Includili in index.html in ordine inverso. Hai terminato:

<!-- index.html -->
<script src="./runtime.79f17c27b335abc7aaf4.js"></script>
<script src="./vendor.26886caf15818fa82dfa.js"></script>
<script src="./main.00bab6fd3100008a42b0.js"></script>

Per approfondire

Runtime webpack in linea per salvare una richiesta HTTP aggiuntiva

Per migliorare ulteriormente, prova a incorporare il runtime webpack nella risposta HTML. Ad esempio, al posto di questo:

<!-- index.html -->
<script src="./runtime.79f17c27b335abc7aaf4.js"></script>

Segui questi passaggi:

<!-- index.html -->
<script>
!function(e){function n(r){if(t[r])return t[r].exports;…}} ([]);
</script>

Il runtime è di dimensioni ridotte e se lo integra puoi salvare una richiesta HTTP (presto molto importante con HTTP/1; meno importante con HTTP/2, ma che potrebbe avere un effetto).

Ecco come fare.

Se generi il codice HTML con il componente HTMLWebpackPlugin

Se utilizzi HtmlWebpackPlugin per generare un file HTML, InlineSourcePlugin ti serve solo:

const HtmlWebpackPlugin = require('html-webpack-plugin');
const InlineSourcePlugin = require('html-webpack-inline-source-plugin');

module.exports = {
  plugins: [
    new HtmlWebpackPlugin({
      inlineSource: 'runtime~.+\\.js',
    }),
    new InlineSourcePlugin()
  ]
};

Se generi HTML utilizzando una logica server personalizzata

Con webpack 4:

  1. Aggiungi WebpackManifestPlugin per conoscere il nome generato del blocco di runtime:

    // webpack.config.js (for webpack 4)
    const ManifestPlugin = require('webpack-manifest-plugin');
    
    module.exports = {
      plugins: [
        new ManifestPlugin()
      ]
    };
    

    Una build con questo plug-in crea un file simile al seguente:

    // manifest.json
    {
      "runtime~main.js": "runtime~main.8e0d62a03.js"
    }
    
  2. Incorpora in modo pratico i contenuti del blocco di runtime. Ad esempio, con Node.js ed Express:

    // server.js
    const fs = require('fs');
    const manifest = require('./manifest.json');
    const runtimeContent = fs.readFileSync(manifest['runtime~main.js'], 'utf-8');
    
    app.get('/', (req, res) => {
      res.send(`
        …
        <script>${runtimeContent}</script>
        …
      `);
    });
    

O con il webpack 3:

  1. Rendi statico il nome del runtime specificando filename:

    module.exports = {
      plugins: [
        new webpack.optimize.CommonsChunkPlugin({
          name: 'runtime',
          minChunks: Infinity,
          filename: 'runtime.js'
        })
      ]
    };
    
  2. Incorpora i contenuti runtime.js in modo pratico. Ad esempio, con Node.js ed Express:

    // server.js
    const fs = require('fs');
    const runtimeContent = fs.readFileSync('./runtime.js', 'utf-8');
    
    app.get('/', (req, res) => {
      res.send(`
        …
        <script>${runtimeContent}</script>
        …
      `);
    });
    

Codice di caricamento lento che non ti serve al momento

A volte una pagina ha parti più e meno importanti:

  • Se carichi la pagina di un video su YouTube, ti interessano di più il video che i commenti. In questo caso il video è più importante dei commenti.
  • Se apri un articolo su un sito di notizie, ti interessa di più il testo dell'articolo che gli annunci. In questo caso, il testo è più importante degli annunci.

In questi casi, migliora le prestazioni di caricamento iniziale scaricando solo gli elementi più importanti per primi e caricando lentamente le parti rimanenti in un secondo momento. Utilizza la funzione import() e la suddivisione del codice per:

// videoPlayer.js
export function renderVideoPlayer() { … }

// comments.js
export function renderComments() { … }

// index.js
import {renderVideoPlayer} from './videoPlayer';
renderVideoPlayer();

// …Custom event listener
onShowCommentsClick(() => {
  import('./comments').then((comments) => {
    comments.renderComments();
  });
});

import() indica che vuoi caricare un modulo specifico in modo dinamico. Quando il webpack rileva import('./module.js'), sposta questo modulo in un blocco separato:

$ webpack
Hash: 39b2a53cb4e73f0dc5b2
Version: webpack 3.8.1
Time: 4273ms
                            Asset     Size  Chunks             Chunk Names
      ./0.8ecaf182f5c85b7a8199.js  22.5 kB       0  [emitted]
   ./main.f7e53d8e13e9a2745d6d.js    60 kB       1  [emitted]  main
 ./vendor.4f14b6326a80f4752a98.js    46 kB       2  [emitted]  vendor
./runtime.79f17c27b335abc7aaf4.js  1.45 kB       3  [emitted]  runtime

e la scarica solo quando l'esecuzione raggiunge la funzione import().

Questa operazione ridurrà le dimensioni del bundle main, migliorando il tempo di caricamento iniziale. Inoltre, migliorerà la memorizzazione nella cache: se modifichi il codice nel blocco principale, il blocco dei commenti non verrà modificato.

Per approfondire

Suddividi il codice in route e pagine

Se la tua app ha più route o pagine, ma è presente un solo file JS con il codice (un unico blocco main), è probabile che vengano forniti byte in più per ogni richiesta. Ad esempio, quando un utente visita una home page del tuo sito:

Una home page di WebFundamentals

non è necessario caricare il codice per visualizzare un articolo che si trova in un'altra pagina, ma lo caricheranno. Inoltre, se l'utente visita sempre solo la home page e modifichi il codice dell'articolo, il webpack renderà non valido l'intero bundle e l'utente dovrà scaricare nuovamente l'intera app.

Se l'app viene suddivisa in pagine (o route, se si tratta di un'app a pagina singola), l'utente scarica solo il codice pertinente. Inoltre, il browser memorizzerà meglio il codice dell'app: se modifichi il codice della home page, il webpack renderà non valido solo il blocco corrispondente.

Per app con una sola pagina

Per suddividere le app con una sola pagina in base alle route, utilizza import() (consulta la sezione "Codice per il caricamento lento che non ti serve al momento"). Se usi un framework, potrebbe esistere una soluzione esistente per questo:

Per le app con più pagine tradizionali

Per suddividere le app tradizionali per pagine, utilizza i punti di ingresso del webpack. Se la tua app ha tre tipi di pagine (home page, pagina dell'articolo e pagina dell'account utente), deve avere tre voci:

// webpack.config.js
module.exports = {
  entry: {
    home: './src/Home/index.js',
    article: './src/Article/index.js',
    profile: './src/Profile/index.js'
  }
};

Per ogni file di voce, webpack creerà un albero delle dipendenze separato e genererà un bundle che include solo i moduli utilizzati da quella voce:

$ webpack
Hash: 318d7b8490a7382bf23b
Version: webpack 3.8.1
Time: 4273ms
                            Asset     Size  Chunks             Chunk Names
      ./0.8ecaf182f5c85b7a8199.js  22.5 kB       0  [emitted]
   ./home.91b9ed27366fe7e33d6a.js    18 kB       1  [emitted]  home
./article.87a128755b16ac3294fd.js    32 kB       2  [emitted]  article
./profile.de945dc02685f6166781.js    24 kB       3  [emitted]  profile
 ./vendor.4f14b6326a80f4752a98.js    46 kB       4  [emitted]  vendor
./runtime.318d7b8490a7382bf23b.js  1.45 kB       5  [emitted]  runtime

Pertanto, se solo la pagina dell'articolo utilizza Lodash, i bundle home e profile non lo includeranno e l'utente non dovrà scaricare questa libreria quando visita la home page.

Tuttavia, gli alberi delle dipendenze separati presentano degli svantaggi. Se due punti di ingresso utilizzano Lodash e non hai spostato le dipendenze in un bundle di fornitori, entrambi i punti di ingresso includeranno una copia di Lodash. Per risolvere questo problema, nel webpack 4 aggiungi l'opzione optimization.splitChunks.chunks: 'all' alla configurazione del webpack:

// webpack.config.js (for webpack 4)
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all'
    }
  }
};

Questa opzione consente la suddivisione intelligente del codice. Con questa opzione, il Webpack cerca il codice comune e lo estrae in file separati.

In alternativa, nel webpack 3, usa l'icona CommonsChunkPlugin, che sposterà le dipendenze comuni in un nuovo file specificato:

module.exports = {
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: 'common',
      minChunks: 2    // 2 is the default value
    })
  ]
};

Gioca con il valore minChunks per trovare il migliore. In genere, è consigliabile mantenerlo basso, ma aumentare se il numero di blocchi cresce. Ad esempio, per 3 blocchi, minChunks potrebbe essere 2, ma per 30 blocchi potrebbe essere 8. Perché se mantieni il valore su 2, troppi moduli entreranno nel file comune, aumentandolo di conseguenza.

Per approfondire

Rendi più stabili gli ID modulo

Quando crea il codice, il webpack assegna un ID a ogni modulo. In seguito, questi ID verranno utilizzati nei require() all'interno del bundle. Di solito, gli ID vengono visualizzati nell'output della build subito prima dei percorsi del modulo:

$ webpack
Hash: df3474e4f76528e3bbc9
Version: webpack 3.8.1
Time: 2150ms
                           Asset      Size  Chunks             Chunk Names
      ./0.8ecaf182f5c85b7a8199.js  22.5 kB       0  [emitted]
   ./main.4e50a16675574df6a9e9.js    60 kB       1  [emitted]  main
 ./vendor.26886caf15818fa82dfa.js    46 kB       2  [emitted]  vendor
./runtime.79f17c27b335abc7aaf4.js  1.45 kB       3  [emitted]  runtime

↓ Qui

[0] ./index.js 29 kB {1} [built]
[2] (webpack)/buildin/global.js 488 bytes {2} [built]
[3] (webpack)/buildin/module.js 495 bytes {2} [built]
[4] ./comments.js 58 kB {0} [built]
[5] ./ads.js 74 kB {1} [built]
+ 1 hidden module

Per impostazione predefinita, gli ID vengono calcolati utilizzando un contatore (ovvero il primo modulo ha ID 0, il secondo ha ID 1 e così via). Il problema è che, quando aggiungi un nuovo modulo, questo potrebbe apparire al centro dell'elenco di moduli, modificando gli ID di tutti i moduli successivi:

$ webpack
Hash: df3474e4f76528e3bbc9
Version: webpack 3.8.1
Time: 2150ms
                           Asset      Size  Chunks             Chunk Names
      ./0.5c82c0f337fcb22672b5.js    22 kB       0  [emitted]
   ./main.0c8b617dfc40c2827ae3.js    82 kB       1  [emitted]  main
 ./vendor.26886caf15818fa82dfa.js    46 kB       2  [emitted]  vendor
./runtime.79f17c27b335abc7aaf4.js  1.45 kB       3  [emitted]  runtime
   [0] ./index.js 29 kB {1} [built]
   [2] (webpack)/buildin/global.js 488 bytes {2} [built]
   [3] (webpack)/buildin/module.js 495 bytes {2} [built]

↓ Abbiamo aggiunto un nuovo modulo...

[4] ./webPlayer.js 24 kB {1} [built]

↓ E guarda cosa è successo! comments.js ora ha l'ID 5 anziché 4

[5] ./comments.js 58 kB {0} [built]

ads.js ora ha l'ID 6 anziché 5

[6] ./ads.js 74 kB {1} [built]
       + 1 hidden module

In questo modo, tutti i blocchi che includono o dipendono da moduli con ID modificati vengono annullati, anche se il codice effettivo non è cambiato. Nel nostro caso, il blocco 0 (il blocco con comments.js) e il blocco main (il blocco con l'altro codice dell'app) vengono invalidati, mentre solo il blocco main avrebbe dovuto essere.

Per risolvere questo problema, modifica il modo in cui vengono calcolati gli ID modulo utilizzando HashedModuleIdsPlugin. Sostituisce gli ID basati su contatore con hash dei percorsi dei moduli:

$ webpack
Hash: df3474e4f76528e3bbc9
Version: webpack 3.8.1
Time: 2150ms
                           Asset      Size  Chunks             Chunk Names
      ./0.6168aaac8461862eab7a.js  22.5 kB       0  [emitted]
   ./main.a2e49a279552980e3b91.js    60 kB       1  [emitted]  main
 ./vendor.ff9f7ea865884e6a84c8.js    46 kB       2  [emitted]  vendor
./runtime.25f5d0204e4f77fa57a1.js  1.45 kB       3  [emitted]  runtime

↓ Qui

[3IRH] ./index.js 29 kB {1} [built]
[DuR2] (webpack)/buildin/global.js 488 bytes {2} [built]
[JkW7] (webpack)/buildin/module.js 495 bytes {2} [built]
[LbCc] ./webPlayer.js 24 kB {1} [built]
[lebJ] ./comments.js 58 kB {0} [built]
[02Tr] ./ads.js 74 kB {1} [built]
    + 1 hidden module

Con questo approccio, l'ID di un modulo cambia solo se rinomini o sposti il modulo. I nuovi moduli non avranno effetto sugli ID di altri moduli.

Per attivare il plug-in, aggiungilo alla sezione plugins del file di configurazione:

// webpack.config.js
module.exports = {
  plugins: [
    new webpack.HashedModuleIdsPlugin()
  ]
};

Per approfondire

Riepilogo

  • Memorizza nella cache il bundle e differenzia le versioni modificandone il nome
  • Suddividi il bundle in codice dell'app, codice del fornitore e runtime
  • Incorporare il runtime per salvare una richiesta HTTP
  • Esegui il caricamento lento del codice non critico con import
  • Suddividi il codice per route/pagine per evitare di caricare contenuti superflui