Usare la memorizzazione nella cache a lungo termine

Vantaggi di Webpack per la memorizzazione nella cache degli asset

L'aspetto successivo (dopo aver ottimizzato le dimensioni dell'app migliora il tempo di caricamento dell'app e la memorizzazione nella cache. Utilizzala per mantenere alcune parti dell'app ed evitare di scaricarli di nuovo ogni volta.

Usa il controllo delle versioni del bundle e le intestazioni cache

L'approccio comune della memorizzazione nella cache consiste nel:

  1. indica al browser di memorizzare nella cache un file per un periodo molto lungo (ad es. un anno):

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

    Se non conosci la funzione di Cache-Control, leggi l'articolo di Jake Archibald eccellente post sulla memorizzazione nella cache pratiche.

  2. e rinomina il file quando viene modificato per forzare 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 copia cache. Il browser raggiungerà la rete solo se il nome del file cambia. (o se passa un anno).

Con webpack fai lo stesso, ma invece di un numero di versione, specifichi la 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 il nome del file per inviarlo al client, usa HtmlWebpackPlugin o WebpackManifestPlugin.

Il HtmlWebpackPlugin è un un approccio semplice, ma meno flessibile. Durante la compilazione, questo plug-in genera File HTML che include tutte le risorse compilate. Se la logica del server non è complesso, per te dovrebbe essere sufficiente:

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

La WebpackManifestPlugin è un approccio più flessibile, utile se si dispone di una parte server complessa. Durante la creazione, genera un file JSON con un mapping tra i nomi dei file. senza hash e nomi dei file con hash. Usa questo JSON sul server per scoprirlo 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 rispetto all'effettivo codice dell'app. Se ti sposti in un file separato, il browser potrà memorizzarle nella cache separatamente. e non verranno scaricati nuovamente ogni volta che cambia il codice dell'app.

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

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

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

    Quando Webpack crea l'app, sostituisce [name] con il nome di un frammento. Se non aggiungiamo la parte [name], avremo differenziare i blocchi in base al loro hash, il che è abbastanza 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 in posizione di [name] nel passaggio 1.

    A questo punto, se crei l'app, questo blocco includerà l'intero codice dell'app, come se non avessimo fatto questi passaggi. Ma questo cambierà in 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 abilita la suddivisione smart code. con cui, webpack estrae il codice del fornitore diventa più grande di 30 kB (prima della minimizzazione e di gzip). Estrarre anche il codice comune, Ciò è utile se la tua build produce diversi bundle (ad es. se suddividi la tua app in percorsi).

    In 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 accetta tutti i moduli che includono i percorsi node_modules e le sposta in un file separato denominato vendor.[chunkhash].js.

Dopo queste modifiche, ogni build genererà due file anziché uno: main.[chunkhash].js e vendor.[chunkhash].js (vendors~main.[chunkhash].js per webpack 4). Nel caso di webpack 4, il bundle del fornitore potrebbe non essere generato se le dipendenze sono ridotte, e va bene così:

$ 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 nella cache questi file separatamente e scarica nuovamente solo il codice che cambia.

Codice di runtime Webpack

Purtroppo, 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: 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 ID chunk e file corrispondenti:

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

Webpack include questo runtime nell'ultimo blocco generato, che è vendor nel nostro caso. E ogni volta che un blocco viene modificato, cambia anche questa porzione di codice, causando il cambiamento dell'intero blocco vendor.

Per risolvere il problema, spostiamo il runtime in un file separato. Nel Webpack 4, ottenuto attivando l'opzione optimization.runtimeChunk:

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

In webpack 3, per farlo crea un blocco aggiuntivo vuoto 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, ogni build genererà 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 nell'ordine inverso. Ecco fatto:

<!-- 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 le cose, prova a incorporare il runtime webpack nell'HTML risposta. Ad esempio, invece di questo:

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

fai questo:

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

Le dimensioni del runtime sono ridotte e la loro presenza consente di salvare una richiesta HTTP ( sono importanti con HTTP/1; meno importante con HTTP/2 ma potrebbe comunque svolgere un ).

Ecco come fare.

Se generi il codice HTML con htmlWebpackplugin

Se utilizzi HtmlWebpackPlugin da generare un file HTML, InlineSourcePlugin è tutto quello di cui hai bisogno:

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 il codice HTML utilizzando una logica server personalizzata

Con Webpack 4:

  1. Aggiungi il parametro 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 creerebbe un file simile al seguente:

    // manifest.json
    {
      "runtime~main.js": "runtime~main.8e0d62a03.js"
    }
    
  2. Incorpora il contenuto del blocco del runtime in modo pratico. Ad es. 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>
        …
      `);
    });
    

Oppure con webpack 3:

  1. Rendi il nome del runtime statico specificando filename:

    module.exports = {
      plugins: [
        new webpack.optimize.CommonsChunkPlugin({
          name: 'runtime',
          minChunks: Infinity,
          filename: 'runtime.js'
        })
      ]
    };
    
  2. Incorpora i contenuti di runtime.js in modo pratico. Ad es. 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 adesso

A volte, una pagina è composta da parti più e meno importanti:

  • Se carichi una pagina video su YouTube, ti interessa di più il video che 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 del invece che sugli annunci. In questo caso, il testo è più importante degli annunci.

In questi casi, per migliorare le prestazioni di caricamento iniziale, scarica solo il file gli elementi più importanti e il caricamento lento delle parti rimanenti in un secondo momento. Utilizza il import() e code-splitting 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 webpack rileva import('./module.js'), sposta questo modulo in un chunk:

$ 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 lo scarica solo quando l'esecuzione raggiunge la funzione import().

In questo modo il bundle main ridurrà le dimensioni, migliorando il tempo di caricamento iniziale. Inoltre, migliorerà la memorizzazione nella cache: se modifichi il codice nel blocco principale, blocco dei commenti non verrà influenzato.

Per approfondire

Suddividi il codice in percorsi e pagine

Se la tua app ha più percorsi o pagine, ma esiste un solo file JS con del codice (un singolo blocco main), è probabile che vengano pubblicati byte aggiuntivi per ogni richiesta. Ad esempio, quando un utente visita la home page del tuo sito:

Una home page di WebFundamentals

non devono caricare il codice per il rendering di un articolo che si trova su un pagina, ma la caricheranno. Inoltre, se l'utente visita sempre solo la casa pagina e si apporta una modifica al codice dell'articolo, webpack invaliderà il l'intero bundle e l'utente dovrà scaricare di nuovo l'intera app.

Se l'app viene suddivisa in pagine (o percorsi, se si tratta di un'app di una sola pagina), l'utente scaricherà solo il codice pertinente. Inoltre, il browser memorizzerà nella cache il codice dell'app migliore: se modifichi il codice della home page, webpack invaliderà solo con il frammento corrispondente.

Per le app di una sola pagina

Per suddividere le app a pagina singola per percorsi, usa import() (consulta la sezione "Codice di caricamento lento) che non ti servono adesso"). Se usi un framework, potrebbe esistere una soluzione per questo problema:

Per le app multipagina tradizionali

Per suddividere le app tradizionali per pagina, utilizza la casella di controllo punti di accesso. 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 delle voci, webpack crea un albero delle dipendenze separato e genera un bundle che include solo moduli utilizzati dalla voce in questione:

$ 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

Quindi, se solo la pagina dell'articolo utilizza Lodash, i bundle home e profile non la includeranno e l'utente non dovrà scaricare questa raccolta quando visitando la home page.

Tuttavia, alberi delle dipendenze separati hanno i loro svantaggi. Se due punti di ingresso utilizzano Lodash e non hai spostato le dipendenze in un bundle di fornitori, entrambe le voci i punti includeranno una copia di Lodash. Per risolvere il problema, in Webpack 4,aggiungi la Opzione optimization.splitChunks.chunks: 'all' nella configurazione webpack:

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

Questa opzione abilita la suddivisione smart code. Con questa opzione, Webpack verrebbe automaticamente cercare il codice comune ed estrarlo in file separati.

In alternativa, in webpack 3,utilizza CommonsChunkPlugin - 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
    })
  ]
};

Divertiti con il valore minChunks per trovare quello migliore. In genere, vuoi mantenerlo basso, ma aumentare se il numero di blocchi cresce. Per Ad esempio, per 3 blocchi, minChunks potrebbe essere 2, ma per 30 blocchi, potrebbe essere 8 – perché se lo mantieni a 2, troppi moduli finiranno nel file comune, gonfiarlo troppo.

Per approfondire

Rendi più stabili gli ID modulo

Durante la creazione del codice, webpack assegna un ID a ciascun modulo. In seguito, questi ID vengono utilizzata in require() all'interno del bundle. Di solito vedi gli ID nell'output della build subito prima dei percorsi dei moduli:

$ 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 (ad es. il primo modulo ha ID 0, il secondo ha ID 1 e così via). Il problema è che, quando aggiungi un nuovo modulo, potrebbe essere visualizzato al centro dell'elenco dei moduli, modificando tutte le moduli successivi ID:

$ 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 una nuova modulo...

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

↓ E guarda cosa ha fatto! Ora comments.js ha l'ID 5 anziché 4

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

ads.js ora ha ID 6 anziché 5

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

Questa operazione invalida tutti i blocchi che includono o dipendono da moduli con ID modificati. 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) invalidato, mentre solo il main avrebbe dovuto esserlo.

Per risolvere il problema, cambia il modo in cui gli ID modulo vengono calcolati utilizzando il metodo HashedModuleIdsPlugin Sostituisce gli ID basati su contatori con hash di percorsi di 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 lo rinomini o sposti in maggior dettaglio più avanti in questo modulo. I nuovi moduli non incideranno su altri moduli ID.

Per attivare il plug-in, aggiungilo alla sezione plugins della configurazione:

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

Per approfondire

Riepilogo

  • Memorizza nella cache il bundle e differenzia tra le versioni modificando il nome del bundle
  • Suddividi il bundle in codice dell'app, codice del fornitore e runtime
  • Incorpora il runtime per salvare una richiesta HTTP
  • Caricamento lento di codice non critico con import
  • Suddividi il codice per route/pagine per evitare di caricare contenuti non necessari