Utiliser la mise en cache à long terme

Avantages de webpack pour la mise en cache des éléments

Après avoir optimisé la taille de l'application, améliore le temps de chargement de l'application est la mise en cache. Utilisez-le pour conserver des parties de l'application sur le et évitez de les télécharger à nouveau à chaque fois.

Utiliser la gestion des versions de bundle et les en-têtes de cache

L'approche courante de la mise en cache consiste à:

  1. au navigateur de mettre en cache un fichier très longtemps (un an, par exemple):

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

    Si vous ne savez pas ce que fait Cache-Control, consultez les excellent post sur la mise en cache. pratiques.

  2. puis renommez le fichier une fois modifié pour forcer le retéléchargement:

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

Cette approche indique au navigateur de télécharger le fichier JS, de le mettre en cache et d'utiliser en cache la copie en cache. Le navigateur n'atteindra le réseau que si le nom du fichier change (ou si une année s'est écoulée).

Avec webpack, vous faites la même chose, mais au lieu d'un numéro de version, vous spécifiez le hachage de fichier. Pour inclure le hachage dans le nom du fichier, utilisez [chunkhash]:

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

Si vous avez besoin nom de fichier pour l'envoyer au client, utilisez la commande HtmlWebpackPlugin ou WebpackManifestPlugin

Le HtmlWebpackPlugin est un simple mais moins flexible. Lors de la compilation, ce plug-in génère Fichier HTML qui inclut toutes les ressources compilées. Si la logique de votre serveur n'est pas il est alors assez complexe pour vous:

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

La WebpackManifestPlugin est une approche plus flexible qui est utile si vous avez une partie de serveur complexe. Pendant la compilation, il génère un fichier JSON avec un mappage entre les noms de fichiers sans hachage et les noms de fichiers avec hachage. Utilisez ce code JSON sur le serveur pour le savoir. le fichier à utiliser:

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

Documentation complémentaire

Extraire les dépendances et l'environnement d'exécution dans un fichier distinct

Dépendances

Les dépendances des applications ont tendance à changer moins souvent que le code réel de l'application. Si vous déménagez dans un fichier séparé, le navigateur pourra les mettre en cache séparément. et ne les télécharge pas à nouveau chaque fois que le code de l'application est modifié.

Pour extraire les dépendances dans un fragment distinct, effectuez trois étapes:

  1. Remplacez le nom de fichier de sortie par [name].[chunkname].js:

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

    Lorsque webpack compile l'application, il remplace [name]. avec le nom d'un fragment. Si nous n'ajoutons pas la partie [name], nous obtenons de différencier les fragments par leur hachage, ce qui est assez difficile !

  2. Convertissez le champ entry en objet:

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

    Dans cet extrait, "main" est le nom d'un bloc. Ce nom sera remplacé dans à la place de [name] de l'étape 1.

    À présent, si vous compilez l'application, ce fragment inclura le code complet de l'application, juste comme si nous n'avions pas fait ces étapes. Mais ça va changer dans une seconde.

  3. Dans webpack 4, ajoutez l'option optimization.splitChunks.chunks: 'all'. dans votre configuration webpack:

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

    Cette option active la division intelligente du code. Webpack extrait alors le code du fournisseur si elle dépasse 30 Ko (avant la minimisation et l'enregistrement gzip). Elle extrait également le code commun : Cela est utile si votre build génère plusieurs bundles (par exemple, si vous divisez votre application en routes).

    Dans webpack 3, ajoutez 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'),
        })
      ]
    };
    

    Ce plug-in utilise tous les modules dont les chemins d'accès incluent node_modules et les déplace dans un fichier distinct appelé vendor.[chunkhash].js.

Après ces modifications, chaque compilation générera deux fichiers au lieu d'un: main.[chunkhash].js et vendor.[chunkhash].js (vendors~main.[chunkhash].js pour Webpack 4). Dans le cas de webpack 4, le bundle du fournisseur peut ne pas être généré si les dépendances sont petites, et ce n'est pas un problème:

$ 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

Le navigateur met alors en cache ces fichiers séparément et ne télécharge à nouveau que le code modifié.

Code d'exécution Webpack

Malheureusement, il ne suffit pas d'extraire le code du fournisseur. Si vous essayez de modifier un élément dans le code de l'application:

// index.js
…
…

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

vous remarquerez que le hachage vendor change également:

                           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

En effet, le bundle webpack, hormis le code des modules, possède un environnement d'exécution : un petit extrait de code qui gère l'exécution du module. Lorsque vous divisez le code en plusieurs fichiers, ce morceau de code commence à inclure un mappage entre les ID de bloc et fichiers correspondants:

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

Webpack inclut cet environnement d'exécution dans le dernier fragment généré, à savoir vendor dans notre cas. Et chaque fois qu'un bloc change, cet extrait de code change également, entraînant la modification de l'ensemble du fragment vendor.

Pour résoudre ce problème, nous allons déplacer l'environnement d'exécution dans un fichier distinct. Dans webpack 4,il s'agit de en activant l'option optimization.runtimeChunk:

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

Dans webpack 3,créez un fragment vide supplémentaire avec 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
    })
  ]
};

Après ces modifications, chaque compilation générera trois fichiers:

$ 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

Ajoutez-les dans index.html dans l'ordre inverse et vous avez terminé:

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

Documentation complémentaire

Environnement d'exécution webpack intégré pour enregistrer une requête HTTP supplémentaire

Pour améliorer les choses, essayez d'intégrer l'environnement d'exécution webpack dans le code HTML. de réponse. Par exemple, au lieu de ceci:

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

procédez comme suit:

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

L'environnement d'exécution est petit, et son intégration vous aidera à enregistrer une requête HTTP (ce qui est important avec HTTP/1. moins important avec HTTP/2, mais il peut tout de même lire un l'effet inverse).

Voici comment procéder.

Si vous générez du code HTML avec le plug-in HTMLWebpackPlugin

Si vous utilisez les HtmlWebpackPlugin, qui permet de générer un fichier HTML, InlineSourcePlugin est tout ce dont vous avez besoin:

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

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

Si vous générez du code HTML à l'aide d'une logique de serveur personnalisée

Avec webpack 4:

  1. Ajoutez le WebpackManifestPlugin pour connaître le nom généré du fragment d'exécution:

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

    Une compilation avec ce plug-in génère un fichier qui se présente comme suit:

    // manifest.json
    {
      "runtime~main.js": "runtime~main.8e0d62a03.js"
    }
    
  2. Intégrez facilement le contenu du fragment d'exécution. Par exemple, avec Node.js et 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>
        …
      `);
    });
    

Ou avec webpack 3:

  1. Rendez le nom de l'environnement d'exécution statique en spécifiant filename:

    module.exports = {
      plugins: [
        new webpack.optimize.CommonsChunkPlugin({
          name: 'runtime',
          minChunks: Infinity,
          filename: 'runtime.js'
        })
      ]
    };
    
  2. Intégrez facilement le contenu runtime.js. Par exemple, avec Node.js et Express:

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

Le code à chargement différé dont vous n'avez pas besoin pour le moment

Parfois, une page contient des parties plus et moins importantes:

  • Si vous chargez une page de vidéo sur YouTube, vous vous intéressez plus à la vidéo commentaires. Ici, la vidéo est plus importante que les commentaires.
  • Si vous ouvrez un article sur un site d'actualités, vous vous souciez davantage du texte plutôt que sur les annonces. Ici, le texte est plus important que les annonces.

Dans ce cas, améliorez les performances de chargement initial en ne téléchargeant que le fichier les éléments les plus importants en premier et le chargement différé des parties restantes plus tard. Utilisez les fonction import() et de code-splitting:

// 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() indique que vous souhaitez charger un module spécifique de manière dynamique. Quand ? webpack voit import('./module.js'), il déplace ce module dans un autre 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

et ne le télécharge que lorsque l'exécution atteint la fonction import().

Le bundle main sera ainsi plus petit, ce qui améliorera le temps de chargement initial. Plus encore, cela améliorera la mise en cache : si vous modifiez le code dans le bloc principal, le bloc de commentaires ne sera pas affecté.

Documentation complémentaire

Diviser le code en plusieurs routes et pages

Si votre application comporte plusieurs routes ou pages, mais qu'il n'y a qu'un seul fichier JS avec le code (un seul bloc main), il est probable que vous utilisiez des octets supplémentaires chaque requête. Par exemple, lorsqu'un utilisateur visite une page d'accueil de votre site:

Une page d&#39;accueil de WebFundamentals

ils n'ont pas besoin de charger le code pour afficher un article qui se trouve sur une autre mais ils la chargent. De plus, si l'utilisateur ne visite que la maison et que vous modifiez le code de l'article, webpack invalide le site l'intégralité du pack, et l'utilisateur devra télécharger à nouveau l'intégralité de l'application.

Si l'application est divisée en pages (ou en routes, s'il s'agit d'une application monopage), l'utilisateur télécharge uniquement le code approprié. De plus, le navigateur met en cache le code de l'application mieux: si vous modifiez le code de la page d'accueil, webpack invalidera uniquement le bloc correspondant.

Pour les applications monopages

Pour diviser les applications monopages en fonction des itinéraires, utilisez import() (voir la section Code de chargement différé dont vous n'avez pas besoin pour le moment"). Si vous utilisez un framework, il existe peut-être une solution à ce problème:

Pour les applications multipages traditionnelles

Pour répartir les applications traditionnelles par pages, utilisez la fonction entrée de webpack points d'accès. Si votre application comporte trois la page d'accueil, la page de l'article et la page du compte utilisateur. doit comporter trois entrées:

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

Pour chaque fichier d'entrée, webpack crée une arborescence de dépendances distincte et génère Un bundle qui ne comprend que les modules utilisés par cette entrée:

$ 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

Ainsi, si seule la page de l'article utilise Lodash, les bundles home et profile ne l'inclut pas, et l'utilisateur n'aura pas à télécharger cette bibliothèque à la page d'accueil.

Toutefois, les arborescences de dépendances distinctes présentent des inconvénients. Si deux points d'entrée utilisent Lodash, et que vous n'avez pas déplacé vos dépendances dans un bundle de fournisseurs, points inclut une copie de Lodash. Pour résoudre ce problème, dans webpack 4,ajoutez le paramètre optimization.splitChunks.chunks: 'all' dans votre configuration webpack:

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

Cette option active la division intelligente du code. Avec cette option, webpack effectue automatiquement rechercher le code commun et l'extraire dans des fichiers distincts.

Vous pouvez également utiliser dans webpack 3 la commande CommonsChunkPlugin. Elle déplacera les dépendances courantes dans un nouveau fichier spécifié:

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

N'hésitez pas à jouer avec la valeur minChunks pour trouver la meilleure. Généralement, vous devez la garder petite, mais augmenter si le nombre de morceaux augmente. Pour Par exemple, pour 3 segments, minChunks peut être égal à 2, mais pour 30 segments, il peut être égal à 8 car si vous conservez la valeur 2, un trop grand nombre de modules arriveront dans le fichier commun. de la gonfler trop.

Documentation complémentaire

Renforcer la stabilité des ID de module

Lors de la création du code, webpack attribue un ID à chaque module. Par la suite, ces identifiants seront utilisée dans les éléments require() du bundle. Vous voyez généralement des ID dans la sortie de compilation juste avant les chemins d'accès au module:

$ 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

↓ Ici

[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

Par défaut, les identifiants sont calculés à l'aide d'un compteur (c'est-à-dire que le premier module a l'identifiant 0, le second porte l'identifiant 1, et ainsi de suite). Le problème avec cela est que lorsque vous ajoutez un nouveau module, il peut apparaître au milieu de la liste des modules et modifier tous les modules suivants 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]

↓ Nous avons ajouté module...

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

↓ Et voilà le résultat ! comments.js a désormais l'ID 5 au lieu de 4

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

ads.js a désormais l'ID 6 au lieu de 5

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

Cela invalide tous les fragments qui incluent ou dépendent de modules dont les ID ont été modifiés. même si leur code réel n'a pas changé. Dans notre cas, le fragment 0 (le fragment avec comments.js) et le bloc main (le bloc contenant l'autre code d'application) obtient invalidée, alors que seule la main aurait dû l'être.

Pour résoudre ce problème, modifiez le mode de calcul des identifiants de module à l'aide de la méthode HashedModuleIdsPlugin Il remplace les ID basés sur un compteur par des hachages de chemins d'accès aux modules:

$ 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

↓ Ici

[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

Ainsi, l'ID d'un module ne change que si vous le renommez ou le déplacez de ce module. Les nouveaux modules n'affectent pas le niveau ID.

Pour activer le plug-in, ajoutez-le à la section plugins de la configuration:

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

Documentation complémentaire

Récapitulatif

  • Mettre en cache le bundle et différencier les versions en modifiant son nom
  • Diviser le bundle en code d'application, code de fournisseur et environnement d'exécution
  • Intégrer l'environnement d'exécution pour enregistrer une requête HTTP
  • Effectuer un chargement différé du code non critique avec import
  • Divisez le code par route/page pour éviter de charger des éléments inutiles