webpack re experience: Performance Optimization

preface

We already know how to use webpack. Now let's talk about how to improve its performance.

1, HMR (module hot load)

When we pack the file, we find that only modifying a small part of the content in one module will repackage the whole. Then if there are 10000 modules in our project, we only modify the content in one module. There is no need to repackage all of them. Just repackage this module, which is what we call hot loading.

  • Style file: HMR function can be used: because style loader is implemented internally
  • js file: HMR function cannot be used by default -- > js code needs to be modified and code supporting HMR function needs to be added
    Note: the HMR function can only handle other files that are not imported js files.
  • html file: HMR function cannot be used by default At the same time, it will cause problems: html files cannot be hot updated (HMR function is not needed)
    Solution: modify the entry entry and import the html file

Here, a property hot:true is added to configure devServer to realize the hot loading of css files.

devServer: {
    contentBase: resolve(__dirname, 'build'),
    compress: true,
    port: 3000,
    open: true,
    // Turn on HMR function
    // When the webpack configuration is modified, the webpack service must be restarted in order for the new configuration to take effect
    hot: true
}

Modifying the entry file configuration can make the html file hot loaded, and add the html file path.

entry: ['./src/js/index.js', './src/index.html'],

Add and implement the hot loading of js in the entry file

 // Once module If hot is true, HMR function is enabled. -- > Make HMR function code effective
if (module.hot) {
   // Method will listen for changes in the a.js file. Once changes occur, other modules will not be repackaged.
    // The following callback function will be executed
  module.hot.accept('./a.js', function() {
  
  });
}

2, source_map

Source map: it is a technology that provides source code to post build code mapping (if the post build code is wrong, the source code error can be tracked through mapping).

Through webpack config. Add the following configuration implementation to the last position in the JS file

devtool: 'eval-source-map'

There are the following parameters

[inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map

Parameter namemap file locationdescribe
source-mapexternalError code accurate information and error location of source code
inline-source-mapinlineOnly one inline source map error code is generated. The exact information and the error location of the source code
hidden-source-mapexternalThe error code is the cause of the error, but there is no error location. The source code error cannot be tracked. Only the error location of the built code can be prompted
eval-source-mapinlineEach file generates a corresponding source map, which is in eval, the accurate information of the error code and the error location of the source code
nosources-source-mapexternalThe error code contains accurate information, but there is no source code information
cheap-source-mapexternalError code accurate information and error location of source code can only be accurate lines
cheap-module-source-mapexternalFor the exact information of the error code and the error location of the source code, the module will add the source map of the loader

It's called inline and external
Inline: the generated map file is in build JS, build JS after each file

External: generate a map file separately

Note: inline builds faster than external

Examples
We will print JS is modified as follows

function print() {
  const content = 'hello print';
  console.log(content)();//An error occurred here
}
export default print;

After running html, report an error directly on the console, as shown in the figure. Click the red marked position to directly jump to the error location in the source code.

Development environment configuration

Two aspects need to be considered in the development environment: fast speed and more friendly debugging

  1. Fast speed (EVAL > inline > heap >...)
    Eval soap map is the fastest
    Eval source map
  2. More friendly debugging
    souce-map
    cheap-module-souce-map
    cheap-souce-map

**It is suggested that both speed and debugging be more friendly: Eval source map / Eval soap module soap map (debugging is not as friendly as the former, but faster)**

Production environment configuration

Production environment needs to consider: should the source code be hidden? Should debugging be more friendly

  • Inlining will make the code larger, so you don't need to inline in the production environment
  • Nosources source map all hidden
  • Hidden source map only hides the source code and will prompt the error message of the built code

Recommended: source map / soap module soap map

3, oneOf

When using webpack to package, each file will go through the configuration options in the rules in the configuration file to see if they are hit. This is unnecessary. Just match one. To achieve this effect, you only need to put the configured loader in the oneOf array, so that when a loader is hit, it will not continue to match the virtual loader.

Note: there cannot be two loaders in the oneOf array to process the same file. If so, you can extract the preferred loader to the outside of the oneOf array. As shown below

 module: {
    rules: [
      {
        // In package Eslintconfig -- > airbnb in JSON
        test: /\.js$/,
        exclude: /node_modules/,
        // Priority implementation
        enforce: 'pre',
        loader: 'eslint-loader',
        options: {
          fix: true
        }
      },
      {
        // Only one of the following loader s will match
        // Note: two configurations cannot handle the same type of file
        oneOf: [
          {
            test: /\.css$/,
            use: [...commonCssLoader]
          },
          {
            test: /\.less$/,
            use: [...commonCssLoader, 'less-loader']
          },
          {
            test: /\.js$/,
            exclude: /node_modules/,
            loader: 'babel-loader',
            options: {
              presets: [
                [
                  '@babel/preset-env',
                  {
                    useBuiltIns: 'usage',
                    corejs: {version: 3},
                    targets: {
                      chrome: '60',
                      firefox: '50'
                    }
                  }
                ]
              ]
            }
          },
          {
            test: /\.(jpg|png|gif)/,
            loader: 'url-loader',
            options: {
              limit: 8 * 1024,
              name: '[hash:10].[ext]',
              outputPath: 'imgs',
              esModule: false
            }
          },
          {
            test: /\.html$/,
            loader: 'html-loader'
          },
          {
            exclude: /\.(js|css|less|html|jpg|png|gif)/,
            loader: 'file-loader',
            options: {
              outputPath: 'media'
            }
          }
        ]
      }
    ]
  }

4, Cache

The idea of caching is to cache the packaged files after the first packaging. If the files are modified, only the changed files will be modified. How to achieve it?

babel cache

You only need to add cacheDirectorv: true when using babel to process js

{
  test: /\.js$/,
  exclude: /node_modules/,
  loader: 'babel-loader',
  options: {
    presets: [
      [
        '@babel/preset-env',
        {
          useBuiltIns: 'usage',
          corejs: { version: 3 },
          targets: {
            chrome: '60',
            firefox: '50'
          }
        }
      ]
    ],
    // Enable babel cache
    // The previous cache is not read until the second build
    cacheDirectory: true
  }
}        

File resource cache

Use hash value

Each time wepack is built, a unique hash value will be generated. The hash value can be used to judge whether the file is repackaged to update the cache.

Problem: because js and css use a hash value at the same time. If I repackage, I may only change one file, but it will invalidate all caches.

output: {
	// Add a hash value to the output file name
    filename: 'js/built.[contenthash:10].js',
    path: resolve(__dirname, 'build')
}

chunkhash

Hash value generated according to chunk. If the package comes from the same chunk, the hash value is the same

Question: the hash values of js and css are still the same, because both css and js are in build js, so they belong to the same chunk. That is to say, js files and css files will be bound together. If css is modified, js will also update the cache.

output: {
    filename: 'js/built.[chunkhash:10].js',
    path: resolve(__dirname, 'build')
}

contenthash

contenthash: generates a hash value according to the contents of the file. The hash value of different files must be different, so that the code can run online and the cache can be used better

output: {
    filename: 'js/built.[contenthash:10].js',
    path: resolve(__dirname, 'build')
}

5, Tree shaking

Understanding: you can think of an application as a tree. The application will introduce a third-party library. However, our application does not use all the third-party library files. Those that are not used can be regarded as dead leaves in the book. Shake the tree to determine which are dead leaves and remove them, so as to reduce the volume of the application.

Premise of use:

  1. ES6 modularization must be used
  2. Open production environment

In package Configuration in JSON

"sideEffects": false All code has no side effects (can be done) tree shaking)

There may be problems with the above configuration: the css / @babel/polyfill (side effect) file may be killed. The following method can be avoided.

// Marked resources will not be processed
//The following * represents any css file and less file
"sideEffects": ["*.css", "*.less"]

6, Code segmentation

Understanding: under normal circumstances, when we use webpack for packaging, no matter how many js files there are in the source file, there is only one js file after the final packaging, and the split code will be packaged into multiple js files, at least realizing on-demand loading.

1. Multi entry file

entry: {
    // Multiple entries: there is one entry, and the final output has a bundle
    index: './src/js/index.js',
    test: './src/js/test.js'
  },
  output: {
    // [name]: take the file name
    filename: 'js/[name].[contenthash:10].js',
    path: resolve(__dirname, 'build')
  }

The above configuration will output index hash. JS and test hash. JS two files.

2,optimization

  1. Node can be_ The code in modules packs a chunk separately for final output
  2. As long as it is multi entry, it will package multiple chunks, and automatically analyze whether there are public files in the multi entry chunk. If so, it will be packaged into a single chunk
 optimization: {
    splitChunks: {
      chunks: 'all'
  }
}

3. Control through js code

Through js code, a file can be packaged into a chunk separately. Import dynamic import syntax: a file can be packaged separately

import(/* webpackChunkName: 'test' */'./test')
  .then(({ mul, count }) => {
    // File loaded successfully~
    // eslint-disable-next-line
    console.log(mul(2, 5));
  })
  .catch(() => {
    // eslint-disable-next-line
    console.log('File loading failed~');
  });

Code segmentation summary

  1. By default, only one file is output for a single entry, and several files are output for multiple entries.
  2. optimization: if it is a single entry, only nodes will be split_ Modules file. If there are multiple entries, it will also analyze whether there are public files. If there are, it will be packaged into a file.
  3. Controlled by js code.

7, Lazy loading

Load only after triggering some conditions, rather than load immediately.

document.getElementById('btn').onclick = function() {
  // Lazy loading ~: the file is loaded only when it needs to be used. In this example, it is loaded only after clicking yes.
  import(/* webpackChunkName: 'test'*/'./test').then(({ mul }) => {
    console.log(mul(4, 5));
  });
};

Normal loading: it can be considered as parallel loading (loading multiple files at the same time)
Preload prefetch: after other resources are loaded, the browser will be idle before use, and then load resources secretly. (poor compatibility)

document.getElementById('btn').onclick = function() {
  import(/* webpackChunkName: 'test', webpackPrefetch: true */'./test').then(({ mul }) => {
    console.log(mul(4, 5));
  });
};

8, PWA

Progressive network development application (accessible offline), using workbox -- > workbox webpack plugin

const WorkboxWebpackPlugin = require('workbox-webpack-plugin');
new WorkboxWebpackPlugin.GenerateSW({
      /*
      Two configurations:
        1. Help serviceworker start quickly
        2. Delete old serviceworker
        Generate a serviceworker configuration file~
      */
      clientsClaim: true,
      skipWaiting: true
})

In index Handling compatibility problems in JS Worker Registration

if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker
      .register('/service-worker.js')
      .then(() => {
        console.log('sw Registration succeeded~');
      })
      .catch(() => {
        console.log('sw Registration failed~');
      });
  });
}

It should be noted that

  1. eslint doesn't know the global variables of window and navigator
    Solution: you need to modify the package eslintConfig configuration in JSON
 "eslintConfig": {
"extends": "airbnb-base",
"env": {
      "browser": true// Support browser side global variables
    }
  }
  1. sw code must run on the server
    –> nodejs
    Step 1: npm i serve -g
    Step 2: serve -s build starts the server and exposes all resources in the build directory as static resources

9, Multi process packaging

First download: thread loader. You need to modify the Babel loader configuration

{
  test: /\.js$/,
  exclude: /node_modules/,
  use: [
      'thread-loader',
      {
      loader: 'babel-loader',
      options: {
        presets: [
          [
            '@babel/preset-env',
            {
              useBuiltIns: 'usage',
              corejs: { version: 3 },
              targets: {
                chrome: '60',
                firefox: '50'
              }
            }
          ]
        ]
      }
    }
  ] 
}

Starting multi process packaging has advantages and disadvantages, because the process startup is about 600ms and the process communication also has overhead. Only when the work takes a long time, multi process packaging is required.

Configuration with controllable number of processes

{
  test: /\.js$/,
  exclude: /node_modules/,
  use: [
    {
      loader: 'thread-loader',
      options: {
        workers: 2 // Open two processes to package
      }
    },
    {
      loader: 'babel-loader',
      options: {
        presets: [
          [
            '@babel/preset-env',
            {
              useBuiltIns: 'usage',
              corejs: { version: 3 },
              targets: {
                chrome: '60',
                firefox: '50'
              }
            }
          ]
        ]
      }
    }
  ]
}

10, externals

Ignore packaging. Ignore the need to manually import the ignored files again after packaging
For example:

externals: {
    // Refuse jQuery to be packaged
    jquery: 'jQuery'
}

CDN can be introduced

11, DLL

Function: indicates that the webpack libraries are not involved in packaging, and the dll will package some libraries separately to avoid repeated packaging, and package the node library into a window.

When you run webpack, you will find webpack by default config. JS configuration file, but we need to run webpack dll. JS file, then execute the following statement to run webpack

 webpack --config webpack.dll.js

webpack.config.js

const { resolve } = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');
module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'built.js',
    path: resolve(__dirname, 'build')
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html'
    }),
    // Tell webpack which libraries are not involved in packaging, and the name of the libraries used will change~
    new webpack.DllReferencePlugin({
      manifest: resolve(__dirname, 'dll/manifest.json')
    }),
    // Package and output a file, and automatically import the resource into html
    new AddAssetHtmlWebpackPlugin({
      filepath: resolve(__dirname, 'dll/jquery.js')
    })
  ],
  mode: 'production'
};

Using dll technology, some libraries (third-party libraries: jquery, react, vue...) are packaged separately. The following configuration is required
webpac.dll.js

const { resolve } = require('path');
const webpack = require('webpack');

module.exports = {
  entry: {
    // [name] -- > jQuery generated by final packaging
    // ['jquery '] -- > the library to be packaged is jQuery
    jquery: ['jquery'],
  },
  output: {
    filename: '[name].js',
    path: resolve(__dirname, 'dll'),
    library: '[name]_[hash]' // What is the name of the content exposed in the packaged library
  },
  plugins: [
    // Package to generate a manifest JSON -- > provides and jquery mapping
    new webpack.DllPlugin({
      name: '[name]_[hash]', // The exposed content name of the mapping library
      path: resolve(__dirname, 'dll/manifest.json') // Output file path
    })
  ],
  mode: 'production'
};

jquery is packaged separately and the mapping relationship file is generated

summary

webpack performance optimization has a lot of content and is a little complex, and you need to try to find the specific difference. Keep going

Tags: Javascript Front-end Webpack

Posted by zhangy on Fri, 06 May 2022 10:18:56 +0300