Desmond

Desmond

An introvert who loves web programming, graphic design and guitar
github
bilibili
twitter

Webpack Performance Optimization

The optimization bottleneck of Webpack mainly lies in two aspects:

  • The Webpack build process takes too long.
  • The resulting bundle size of Webpack is too large.

Strategies to Speed Up the Build Process#

Don't let loaders do too much work - using babel-loader as an example#

The most common optimization method is to use the include or exclude options to avoid unnecessary transpilation. For example, the official Webpack documentation provides the following example for babel-loader:

module: {
  rules: [
    {
      test: /\.js$/,
      exclude: /(node_modules|bower_components)/,
      use: {
        loader: 'babel-loader',
        options: {
          presets: ['@babel/preset-env']
        }
      }
    }
  ]
}

This code helps us avoid processing the large node_modules or bower_components folders. However, the performance improvement from limiting the file range is limited. In addition, if we choose to enable caching and cache the transpilation results to the file system, we can at least double the efficiency of babel-loader. To do this, we just need to add the corresponding parameter setting to the loader:

loader: 'babel-loader?cacheDirectory=true'

Don't overlook third-party libraries#

There are many ways to handle third-party libraries, among which the CommonsChunkPlugin will rebuild the vendor bundle every time. For efficiency reasons, we often use DllPlugin instead.

DllPlugin is based on the concept of Windows dynamic link libraries (dll). This plugin will package third-party libraries into a separate file, which is a pure dependency library. This dependency library will not be repackaged with your business code unless the dependency itself changes.

To handle files with DllPlugin, we need to follow two steps:

  • Package the dll library based on a dedicated configuration file for dll.
  • Package the business code based on the webpack.config.js file.

Taking a simple React-based project as an example, our dll configuration file can be written as follows:

const path = require('path')
const webpack = require('webpack')

module.exports = {
    entry: {
      // Array of dependent libraries
      vendor: [
        'prop-types',
        'babel-polyfill',
        'react',
        'react-dom',
        'react-router-dom',
      ]
    },
    output: {
      path: path.join(__dirname, 'dist'),
      filename: '[name].js',
      library: '[name]_[hash]',
    },
    plugins: [
      new webpack.DllPlugin({
        // The name property of DllPlugin needs to be consistent with the library
        name: '[name]_[hash]',
        path: path.join(__dirname, 'dist', '[name]-manifest.json'),
        // The context needs to be consistent with webpack.config.js
        context: __dirname,
      }),
    ],
}

After writing this configuration file, running it will generate a file named vendor-manifest.json in our dist folder, which describes the specific paths of each third-party library. Then, we just need to make some configurations for dll in webpack.config.js:

const path = require('path');
const webpack = require('webpack')
module.exports = {
  mode: 'production',
  // Entry point for compilation
  entry: {
    main: './src/index.js'
  },
  // Output file
  output: {
    path: path.join(__dirname, 'dist/'),
    filename: '[name].js'
  },
  // Dll-related configurations
  plugins: [
    new webpack.DllReferencePlugin({
      context: __dirname,
      // The manifest is the json file we packaged in the first step
      manifest: require('./dist/vendor-manifest.json'),
    })
  ]
}

Happypack - transforming loaders from single-threaded to multi-threaded#

As we know, Webpack is single-threaded, so even if there are multiple tasks at the moment, you can only wait in line for one task to be processed after another. This is a drawback of Webpack, but fortunately, our CPUs are multi-core. Happypack fully leverages the advantages of multi-core concurrency in CPUs and distributes tasks to multiple child processes to improve the efficiency of bundling.

HappyPack is very easy to use. We just need to move the loader configuration to HappyPack. We can manually specify how many concurrent processes we need:

const HappyPack = require('happypack')
// Manually create a thread pool
const happyThreadPool =  HappyPack.ThreadPool({ size: os.cpus().length })

module.exports = {
  module: {
    rules: [
      ...
      {
        test: /\.js$/,
        // The query parameter after the question mark specifies the name of the HappyPack instance that handles this type of file
        loader: 'happypack/loader?id=happyBabel',
        ...
      },
    ],
  },
  plugins: [
    ...
    new HappyPack({
      // The "name" of this HappyPack is called happyBabel, corresponding to the query parameter above
      id: 'happyBabel',
      // Specify the thread pool
      threadPool: happyThreadPool,
      loaders: ['babel-loader?cacheDirectory']
    })
  ],
}

Compressing the Bundle Size#

Visualize the file structure to identify the causes of large bundle sizes#

A very useful package for visualizing the file structure is webpack-bundle-analyzer. The configuration method is the same as other plugins. It presents the sizes and dependencies of each module within the bundle in the form of a rectangular tree diagram. To use it, we just need to import it as a plugin:

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
 
module.exports = {
  plugins: [
    new BundleAnalyzerPlugin()
  ]
}

Remove redundant code#

Based on the import/export syntax, Tree-Shaking can determine which modules are not actually used during the compilation process. These unused codes will be removed during the final bundling. Tree-Shaking is highly targeted and is more suitable for handling module-level redundant code. As for removing more granular redundant code, it is often integrated into the compression or separation process of JS or CSS.

Here, we take UglifyJsPlugin, which is widely accepted, as an example to see how to automatically remove fragmented redundant code (such as console statements, comments, etc.) during the compression process:

const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
module.exports = {
 plugins: [
   new UglifyJsPlugin({
     // Enable parallel processing
     parallel: true,
     // Enable caching
     cache: true,
     compress: {
       // Remove all console statements
       drop_console: true,
       // Automatically define frequently used static values as variables
       reduce_vars: true,
     },
     output: {
       // Do not keep comments
       comment: false,
       // Make the output code as compact as possible
       beautify: false
     }
   })
 ]
}

This code that manually imports UglifyJsPlugin is actually the usage in Webpack 3. In Webpack 4, uglifyjs-webpack-plugin is already used by default for code compression. In Webpack 4, we customize compression-related operations by configuring optimization.minimize and optimization.minimizer.

Code splitting#

  • Do not load all file contents at once, only load the part that is needed at the moment (split in advance).
  • When more content is needed, load the content that is needed at that time.

When we don't need code splitting, our code looks like this:

import BugComponent from '../pages/BugComponent'
...
<Route path="/bug" component={BugComponent}>

To enable code splitting, we need to make some changes. First, we need to configure the webpack.config.js file:

output: {
    path: path.join(__dirname, '/../dist'),
    filename: 'app.js',
    publicPath: defaultSettings.publicPath,
    // Specify chunkFilename
    chunkFilename: '[name].[chunkhash:5].chunk.js',
},

We also need to make some changes to the code in the router:

const getComponent => (location, cb) {
  // Core method
  require.ensure([], (require) => {
    cb(null, require('../pages/BugComponent').default)
  }, 'bug')
},
...
<Route path="/bug" getComponent={getComponent}>

require.ensure(dependencies, callback, chunkName) is an asynchronous method. During the bundling process, BugComponent will be bundled into a separate file. The callback of this asynchronous method will only be triggered when we navigate to the bug route, and it will actually fetch the content of BugComponent. This is code splitting. Code splitting fundamentally means triggering the corresponding callback at the right time.


References:
https://juejin.cn/book/6844733750048210957/section/6844733750102720526

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.