• Original address: use long term caching
  • Original author: Ivan Akulov
  • Translation Address: Take advantage of persistent caching
  • Translator: Zhou Wenkang
  • Proofreader: Yan Meng, Ni Kun

After optimizing the size of your application, the next strategy to improve the load time of your application is caching. Caching resources in the client avoids having to re-download them each time.

Bundle versioning and use of cache headers

A common way to use caching:

  1. Tell the browser to cache a file for a long time (say, a year)

    # Server header
    Cache-Control: max-age=31536000
    Copy the code

    ⭐️ Note: If you are not familiar with the principles of cache-control, see Jake Archibald’s article: Best Practices on Caching.

  2. When the file changes, the file is renamed, which forces the browser to re-download:

    <! -- Before modifying --><script src="./index-v15.js"></script><! -- After modification --><script src="./index-v16.js"></script>
    
    Copy the code

This method tells the browser to download the JS file, cache it, and then use a cached copy of it. The browser will only request the network if the file name changes (or if the cache expires after a year).

With Webpack, you can do the same, but instead of using the version number, you specify the file’s hash value. Use [chunkhash] to write hash values to file names:

// webpack.config.js
module.exports = {
  entry: './index.js'.output: {
    filename: 'bundle.<strong>[chunkhash]</strong>.js'./ / to bundle. 8 e0d62a03. Js}};Copy the code

⭐️ Note: WebPack may generate different hashes even if the bundle does not change – for example, if you rename a file or compile the bundle on a different operating system. Of course, this is actually a bug, and there is no clear solution yet, see the discussion on GitHub for details.

If you need to send the filename to the client, you can use the HtmlWebpackPlugin or the WebpackManifestPlugin.

HtmlWebpackPlugin is a simple but not strong extensible plug-in. During compilation, it generates an HTML file containing all the resources that have been compiled. If your server-side logic is not too complicated, it should satisfy you:

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

The WebpackManifestPlugin is a more extensible plug-in that can help you solve the more complex parts of the server-side logic. When packaged, it generates a JSON file containing the original file name and the mapping with the hash file name. On the server side, we can easily find the file we want to execute using JSON:

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

Further reading

  • Jake Archibald’s best practices on caching

Extract the dependencies and Runtime into separate files

Rely on

Application dependencies are usually less frequent than code changes in the actual application. If you move them to a separate file, the browser can cache them independently — so you don’t have to re-download them every time the code changes in your application.

Key term: In Webpack terminology, separate files with application code are called chunks. We will use this name in the following articles.

To extract dependencies into separate chunks, perform the following three steps:

  1. Replace the output file name with [name].[chunkName].js:

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

When WebPack compiles the application, it uses [name] as the name of chunk. If we hadn’t added the [name] part, we would have had to hash the chunk apart – and that would have been very difficult!

  1. Change the value of entry to an object:

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

    In the above code, “main” is the name of chunk. This name will be replaced by [name] in the first step.

    So far, if you’re building an application, this chunk contains the entire application code – just as we haven’t done any of these steps. But things will change soon.

  2. In webpack 4, optimization can be splitChunks. Chunks: ‘all’ option added to the webpack configuration:

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

    This option enables intelligent code splitting. Using this feature, WebPack will extract third-party library code greater than 30KB (before compression and gzip). It can also extract common code – useful if your build results in multiple bundles. (For example, if you split your application by route).

    Add CommonsChunkPlugin to WebPack 3:

    // webpack.config.js (for webpack 3)
    module.exports = {
      plugins: [
        new webpack.optimize.CommonsChunkPlugin({
          // The name of chunk will contain dependencies
          // This name will be replaced by [name] in the first step
          name: 'vendor'.// This function determines which modules will be added to chunk
          minChunks: module= > module.context &&
            module.context.includes('node_modules'),})]};Copy the code

    The plugin moves all modules whose path contains node_modules into a separate file named vendor.[chunkhash].js.

With these changes, each package will generate two files instead of one: Main. [chunkhash].js and vendor.[chunkhash].js (vendors~main.[chunkhash].js are only available in WebPack 4). In WebPack 4, if the dependency is small, vendor bundles may not be generated – this does a good job:

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

The browser caches these files separately – and redownloads them only if the code changes.

Webpack runtime code

Unfortunately, it’s not enough to just extract third-party library code. If you want to try and change something in your application code:

// index.js... ...// For example, add this sentence:
console.log('Wat');
Copy the code

You’ll notice that vendor’s hash value is also changed:

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

left

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

This is because when WebPack is packaged, in addition to the module code, the WebPack bundle also contains the Runtime – a small piece of code that manages the module’s execution. When you split code into multiple files, this small piece of code generates a mapping between the chunk ID and the matching file:

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

Webpack includes the Runtime in the newly generated chunk, which is the vendor in our code. Every time there is any change of chunk, this small part of the code will also be changed, and the whole Vendor Chunk will also be changed.

To solve this problem, we can move the Runtime into a separate file. In webpack 4, can open optimization runtimeChunk options to achieve:

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

In WebPack 3, an additional empty chunk can be created using the 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 be executed after vendor generation (because WebPack calls the runtime into the latest chunk)
    new webpack.optimize.CommonsChunkPlugin({
      name: 'runtime'.// minChunks: Infinity indicates that no application module can access this chunk
      minChunks: Infinity,})]};Copy the code

After these changes are made, three files will be generated per build:

$ webpack
Hash: ac01483e8fec1fa70676
Version: webpack 3.81.
Time: 3816ms
                            Asset     Size  Chunks             Chunk Names
   ./main00.bab6fd3100008a42b0.js    82 kB       0  [emitted]  main
 ./vendor26886.caf15818fa82dfa.js    46 kB       1  [emitted]  vendor
./runtime79.f17c27b335abc7aaf4.js  1.45 kB       3  [emitted]  runtime
Copy the code

Add these files to index.html in reverse order, and you’re done:

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

Further reading

  • Webpack guide on persistent caching
  • Webpack documentation About the Webpack runtime and manifest file
  • “CommonsChunkPlugin Best Practices”
  • optimization.splitChunksoptimization.runtimeChunkThe working principle of

Inlining webpack runtime saves extra HTTP requests

For a better experience, try inlining the WebPack Runtime into HTML. For example, let’s not do this:

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

Instead, it looks like this:

<! -- index.html --><script>
!function(e){function n(r){if(t[r])returnt[r].exports; ... }} ([]);</script>
Copy the code

Runtime doesn’t have much code, and inlining HTML can help save HTTP requests (especially important in HTTP/1; Less important in HTTP/2, but still useful).

Here’s how to do it.

If you use HtmlWebpackPlugin to generate HTML

If you use HtmlWebpackPlugin to generate HTML files, you must use the InlineSourcePlugin:

// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const InlineSourcePlugin = require('html-webpack-inline-source-plugin');

module.exports = {
  plugins: [
    new HtmlWebpackPlugin({
      // Inline all files which names start with "runtime~" and end with ".js"
      // That’s the default naming of runtime chunks
      inlineSource: 'runtime~.+\\.js',}).// This plugin enables the “inlineSource” option
    new InlineSourcePlugin(),
  ],
};
Copy the code

If you use custom server logic to generate HTML

In WebPack 4:

  1. Add the WebpackManifestPlugin to obtain the name of the generated Runtume Chunk:

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

    Using this plug-in build produces files like the following:

    // manifest.json
    {
      "runtime~main.js": "runtime~main.8e0d62a03.js"
    }
    Copy the code
  2. You can inline runtime Chunk content in a convenient way. For example, using Node.js and 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(`... &lt; script>${runtimeContent}&lt; / script >... `);
    });
    Copy the code

In WebPack 3:

  1. By specifying filename, the runtime name does not change:

    // webpack.config.js (for webpack 3)
    module.exports = {
      plugins: [
        new webpack.optimize.CommonsChunkPlugin({
          name: 'runtime'.minChunks: Infinity.filename: 'runtime.js'.// → Now the runtime file will be called
            / / "runtime. Js", not "runtime. 79 f17c27b335abc7aaf4. Js"})]};Copy the code
  2. You can inline the contents of runtime.js in a convenient way. For example, using Node.js and Express:

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

Lazy code loading

Usually, a web page will have its own focus:

  • If you load a video page on YouTube, you care more about the video than the comments. So the video is more important than the comment.
  • Or if you’re reading an article on a news website, you’re more interested in the text of the article than the advertisement. So, text is more important than advertising.

In all of these cases, you can improve the performance of the first page load by downloading the most important parts first and lazily loading the rest later. In Webpack, this is done using the import() function and code splitting.

// videoPlayer.js
export function renderVideoPlayer() {... }// comments.js
export function renderComments() {... }// index.js
import {renderVideoPlayer} from './videoPlayer';
renderVideoPlayer();

/ /... Custom event listener
onShowCommentsClick((a)= > {
  import('./comments').then((comments) = > {
    comments.renderComments();
  });
});
Copy the code

The import() function helps you load on demand. When Webpack encounters import(‘./module.js’) during packaging, it puts the module in a separate chunk:

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

It is only downloaded when the code executes into the import() function.

This makes the entry bundle smaller, reducing the first load time. Not only that, but it also optimizes the cache – if you change the code for the entry chunk, the comment chunk won’t be affected.

⭐️ Note: If you compile code using Babel, you will get a syntax error because Babel does not recognize import(). To avoid this error, you can add the syntax-dynamic-import plug-in.

Further reading

  • Use of the Webpack document import() function
  • The JavaScript proposal implements the import() syntax

Split the code into routes and pages

If your application has multiple routes or pages, but only a single JS file (a single chunk of entry) in the code, this seems to add extra traffic to every request you make. For example, when a user visits the front page of your website:

They don’t need to load the code that renders articles on other pages – but they do. In addition, if the user frequently visits only the home page, but you change the article code for other pages, WebPack will recompile, invalidating the entire bundle – which will cause the user to re-download the entire application code.

If we split the code into pages (or routes for single-page applications), users will only download the part of the code they really need. Also, browsers cache application code better: When you change the code on the front page, WebPack invalidates only the chunks that match.

Single page application

To route a single page application, use import() (see code lazy loading above). If you’re using a framework, there are already solutions:

  • react-routerIn the document”Code separation”(适用于 React)
  • vue-routerIn the document”Lazy loading of routes”(适用于 Vue.js)

Traditional multi-page applications

To split traditional applications by page, use Entry Points for Webpack. Suppose you have three types of pages in your app: the home page, the Articles page, and the user account page — then there should be three entries:

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

For each entry file, WebPack builds a separate dependency tree and generates a bundle that contains only the modules used by the entry:

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

So, if only the Article page uses Lodash, then the Home and profile bundle will not include it – and users will not download the library when they visit the home page.

However, separate dependency trees have their drawbacks. If both entries use Lodash and you do not move the dependency into the Vendor bundle, both entries will contain copies of Lodash. In order to solve this problem, in webpack 4, can join in your webpack configuration optimization. SplitChunks. Chunks: ‘all options:

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

This option enables intelligent code splitting. With this option, WebPack will automatically find the common code and extract it into a separate file.

In WebPack 3, you can use the CommonsChunkPlugin, which moves the common dependencies to a new specified file:

// webpack.config.js (适用于 webpack 3)
module.exports = {
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      // The name of chunk will contain public dependencies
      name: 'common'.// minChunks indicates the number of 'minChunks' that must be included in order to dump a module into a public file
      // (Note that the plugin analyzes all chunks and entries)
      minChunks: 2.// 2 is the default value})]};Copy the code

You can try adjusting the value of minChunks to find the best solution. Normally, you want it to be a small value, but it increases as the number of chunks increases. For example, with 3 chunks, minChunks might have a value of 2, but with 30 chunks it might have a value of 8 – because if you set it to 2, there will be so many modules packed into the same common file that the file will become bloated.

Further reading

  • The concept of Entry points in Webpack documentation
  • Webpack documentation about the CommonsChunkPlugin plugin
  • “CommonsChunkPlugin Best Practices”
  • optimization.splitChunksoptimization.runtimeChunkThe working principle of

Ensure that the module ID is more stable

When you build code, WebPack assigns an ID to each module. These ids are then used in the bundle’s require() function. You usually see these ids before compiling the output module path:

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

Left to see here

   [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
Copy the code

By default, these ids are computed using a counter (for example, the ID of the first module is 0, the ID of the second module is 1, and so on). The problem with this is that when you add a module, it may appear in the middle of the module list, causing the ID of all modules to change later:

$ webpack
Hash: df3474e4f76528e3bbc9
Version: webpack 3.81.
Time: 2150ms
                           Asset      Size  Chunks             Chunk Names
      ./0.5c82c0f337fcb22672b5.js    22 kB       0  [emitted]
   ./main. 0c8b617dfc40c2827ae3.js    82 kB       1  [emitted]  main
 ./vendor26886.caf15818fa82dfa.js    46 kB       2  [emitted]  vendor
./runtime79.f17c27b335abc7aaf4.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]
Copy the code

↓ We have added a new module…

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

↓ Take a look at the following! The ID of comments.js has changed from 4 to 5

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

↓ Ads. js ID changed from 5 to 6

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

This invalidates all chunks that contain or depend on modules with these changed ids – even if their actual code hasn’t changed. In our case, both chunk 0 (chunk of comments.js) and main Chunk (chunk of other application code) will fail – but only main should fail.

To solve this problem, the HashedModuleIdsPlugin plug-in can be used to change how the module ID is computed. This plugin replaces the counter – based ID with a hash of the module’s path:

$ webpack
Hash: df3474e4f76528e3bbc9
Version: webpack 3.81.
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
./runtime25.f5d0204e4f77fa57a1.js  1.45 kB       3  [emitted]  runtime
Copy the code

Left to see here

[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
Copy the code

With this method, the module ID changes only when the module is renamed or moved. The new module also does not affect the ids of other modules.

This plug-in can be turned on in the plugins section of the configuration:

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

Further reading

  • The Webpack documentation is about HashedModuleIdsPlugin

conclusion

  • Caching bundles and versioning by changing the bundle name
  • Split the bundle into app code, Vendor code, and Runtime
  • Inline Runtime saves HTTP requests
  • useimportLazy loading of non-critical code
  • Break up the code by route or page to avoid loading unnecessary files

To share more, please follow YFE:

Google – Web Performance Optimization with WebPack (I) : Reduce the front-end resource size

Google – Web Performance Optimization with WebPack (III) : Monitoring and analyzing applications