Utiliser la mise en cache à long terme

Utilité du pack Web pour la mise en cache des éléments

L'étape suivante (après avoir optimisé la taille de l'application) qui améliore le temps de chargement de l'application est la mise en cache. Utilisez-le pour conserver certaines parties de l'application sur le client et éviter de les télécharger à nouveau à chaque fois.

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

L'approche courante pour effectuer la mise en cache est la suivante:

  1. indiquer au navigateur de mettre un fichier en cache pendant une très longue période (par exemple, une année):

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

    Si vous ne connaissez pas le rôle de Cache-Control, consultez l'excellent article de Jake Archibal sur les bonnes pratiques de mise en cache.

  2. et renommez le fichier lorsqu'il est modifié pour forcer le nouveau té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 la copie mise en cache. Le navigateur n'atteindra le réseau que si le nom du fichier change (ou si une année passe).

Avec webpack, vous faites la même chose, mais au lieu d'un numéro de version, vous spécifiez le hachage du 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 du nom du fichier pour l'envoyer au client, utilisez HtmlWebpackPlugin ou WebpackManifestPlugin.

L'approche HtmlWebpackPlugin est simple, mais moins flexible. Lors de la compilation, ce plug-in génère un fichier HTML qui inclut toutes les ressources compilées. Si la logique du serveur n'est pas complexe, elle devrait vous suffire:

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

L'approche WebpackManifestPlugin est plus flexible et peut être utile si vous disposez d'une partie serveur complexe. Lors de 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 savoir avec quel fichier utiliser:

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

Complément d'informations

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 les déplacez dans un fichier distinct, le navigateur pourra les mettre en cache séparément et ne les téléchargera pas à nouveau à chaque modification du code de l'application.

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] par le nom d'un fragment. Si nous n'ajoutons pas la partie [name], nous devrons 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 fragment. Ce nom sera remplacé par [name] défini à l'étape 1.

    À présent, si vous compilez l'application, ce fragment comprend l'ensemble du code de l'application, tout comme nous n'avons pas effectué ces étapes. Mais cela va changer dans quelques secondes.

  3. Dans le pack Webpack 4, ajoutez l'option optimization.splitChunks.chunks: 'all' à votre configuration Webpack:

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

    Cette option active le fractionnement intelligent du code. Il permet à webpack d'extraire le code du fournisseur s'il dépasse 30 Ko (avant la minimisation et gzip). Il extraira également le code commun, ce qui est utile si votre compilation 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 prend tous les modules dont les chemins d'accès incluent node_modules et les déplace dans un fichier distinct appelé vendor.[chunkhash].js.

Une fois ces modifications effectuées, chaque compilation génère deux fichiers au lieu d'un seul: main.[chunkhash].js et vendor.[chunkhash].js (vendors~main.[chunkhash].js pour webpack 4). Dans le cas de webpack 4, le bundle de 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 mettrait ces fichiers en cache séparément et ne téléchargerait à 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, à l'exception du code des modules, dispose d'un environnement d'exécution, c'est-à-dire d'un petit morceau de code qui gère l'exécution du module. Lorsque vous divisez le code en plusieurs fichiers, cet extrait de code commence à inclure un mappage entre les ID de fragment et les 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é, qui est vendor dans notre cas. Chaque fois qu'un fragment change, cet extrait de code change également, ce qui entraîne la modification de l'ensemble du fragment vendor.

Pour résoudre ce problème, déplacez l'environnement d'exécution dans un fichier distinct. Dans webpack 4,cela est possible 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 build 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

Incluez-les dans index.html dans l'ordre inverse. Vous avez terminé:

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

Complément d'informations

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

Pour améliorer encore les choses, essayez d'intégrer l'environnement d'exécution webpack dans la réponse HTML. 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 (très important avec HTTP/1 ; moins important avec HTTP/2, mais peut quand même jouer un effet).

Voici comment procéder.

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

Si vous utilisez HtmlWebpackPlugin pour générer un fichier HTML, il vous suffit d'utiliser le plug-in InlineSourcePlugin:

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 le pack 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 crée un fichier qui se présente comme suit:

    // manifest.json
    {
      "runtime~main.js": "runtime~main.8e0d62a03.js"
    }
    
  2. Intégrez le contenu du fragment d'environnement d'exécution de manière pratique. 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>
        …
      `);
    });
    

Chargez du code différé dont vous n'avez pas besoin pour le moment.

Parfois, une page comporte des parties plus ou moins importantes:

  • Lorsque vous chargez une page de vidéo sur YouTube, vous vous souciez davantage de la vidéo que des 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 plus du texte de l'article que des annonces. Ici, le texte est plus important que les annonces.

Dans ce cas, pour améliorer les performances de chargement initial, téléchargez d'abord les éléments les plus importants, puis chargez les autres de manière différée. Pour ce faire, utilisez la fonction import() et la division du code:

// 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. Lorsque Webpack voit import('./module.js'), il déplace ce module dans un fragment distinct:

$ 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 la 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. De plus, cela améliorera la mise en cache. Si vous modifiez le code dans le fragment principal, le fragment de commentaires ne sera pas affecté.

Complément d'informations

Diviser le code en routes et en 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 diffusiez des octets supplémentaires sur chaque requête. Par exemple, lorsqu'un utilisateur visite une page d'accueil de votre site:

Une page d&#39;accueil WebFundamentals

ils n'ont pas besoin de charger le code pour afficher un article situé sur une autre page, mais ils le chargent. De plus, si l'utilisateur ne consulte toujours que la page d'accueil et que vous modifiez le code de l'article, webpack invalidera l'ensemble du bundle et l'utilisateur devra télécharger à nouveau l'ensemble de l'application.

Si nous divisons l'application en pages (ou en routes, s'il s'agit d'une application monopage), l'utilisateur ne télécharge que le code approprié. De plus, le navigateur met mieux en cache le code de l'application: si vous modifiez le code de la page d'accueil, webpack n'invalide que le fragment correspondant.

Pour les applications monopages

Pour diviser les applications monopages par routes, utilisez import() (consultez 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 déjà une solution pour y parvenir:

Pour les applications multipages traditionnelles

Pour diviser les applications traditionnelles par pages, utilisez les points d'entrée du pack Webpack. Si votre application comporte trois types de pages (page d'accueil, page d'article et page de compte utilisateur), elle 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 ne contenant 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 d'article utilise Lodash, les bundles home et profile ne l'incluront pas. L'utilisateur n'aura pas besoin de télécharger cette bibliothèque lorsqu'il visitera la page d'accueil.

Les arborescences de dépendances distinctes présentent toutefois 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, les deux points d'entrée incluront une copie de Lodash. Pour résoudre ce problème, dans Webpack 4,ajoutez l'option optimization.splitChunks.chunks: 'all' à la configuration de votre pack Web:

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

Cette option active le fractionnement intelligent du code. Avec cette option, webpack recherche automatiquement le code commun et l'extrait dans des fichiers distincts.

Ou, dans le pack Web 3, utilisez CommonsChunkPlugin, qui 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. En règle générale, il est préférable qu'il soit petit, mais augmentez le nombre de fragments si le nombre de fragments augmente. Par exemple, pour trois fragments, la valeur de minChunks peut être égale à 2, mais à 8 pour 30 segments. En effet, si vous la limitez à 2, un trop grand nombre de modules entrera dans le fichier commun et le gonflera trop.

Complément d'informations

Renforcer la stabilité des ID de module

Lors de la création du code, webpack attribue un ID à chaque module. Par la suite, ces ID seront utilisés dans les require() du bundle. Les ID s'affichent généralement dans le résultat de compilation juste avant les chemins d'accès aux modules:

$ 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 ID sont calculés à l'aide d'un compteur (autrement dit, le premier module a l'ID 0, le deuxième l'ID 1, et ainsi de suite). Le problème est que lorsque vous ajoutez un nouveau module, il peut s'afficher au milieu de la liste des modules, modifiant tous les ID des modules suivants:

$ 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é un nouveau module...

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

↓ Et regarde ce que ça a fait ! L'ID comments.js est désormais 5 au lieu de 4

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

↓ L'ID de ads.js est désormais 6 au lieu de 5

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

Cette opération invalide tous les fragments qui incluent ou dépendent de modules avec des ID 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 fragment main (le fragment avec l'autre code d'application) sont invalidés, alors que seul le fragment main aurait dû l'être.

Pour résoudre ce problème, modifiez le mode de calcul des ID de module à l'aide de HashedModuleIdsPlugin. Elle remplace les ID basés sur des compteurs par des hachages de chemins de module:

$ 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

Avec cette approche, l'ID d'un module ne change que si vous le renommez ou le déplacez. Les nouveaux modules n'affectent pas les ID des autres modules.

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

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

Complément d'informations

Récapitulatif

  • Mettre en cache le bundle et différencier les versions en modifiant le nom du bundle
  • Diviser le bundle en code d'application, code fournisseur et environnement d'exécution
  • Intégrer l'environnement d'exécution pour enregistrer une requête HTTP
  • Charger du code non critique de manière différée avec import
  • Répartissez le code par routes/pages pour éviter de charger des éléments inutiles.