Skip to content

Project optimization solution in Webpack scenario

About 1648 wordsAbout 5 min

nodewebpack

2022-10-11

webpack

In a front-end project based on webpack as a build tool, there are usually two aspects to optimize.

  1. [Compilation and build time optimization] (#Compilation and build time optimization)
  2. [Build product optimization] (#Build product optimization)

Compilation and build time optimization

Compile build time optimization, aiming to speed up each build and reduce build time. It includes the time overhead for each modification file recompilation during development; the overall time overhead for building the final product for the project.

Optimization directions include:

  1. [Compilation and build time optimization] (#Compilation and build time optimization).
  2. [Reduce file matching range] (#Reduce file matching range).
  3. [File suffix matching] (#File suffix matching).
  4. Cache.
  5. [Parallel build] (#parallel build).

Reduce file matching range

When configuring webpack loader, two properties are usually specified: test and use to declare which files need to be converted.

module.exports = {
  module: {
    rules: [{ test: /\.txt$/, use: 'raw-loader' }],
  },
}

By default, the matching lookup range is searching relative to the context of the project root directory, which can be very time-consuming when the project files are high. In this case, you can use the include and exclude properties to limit the file matching range.

const path = require('node:path')
module.exports = {
  module: {
    rules: [
      {
        test: /\.txt$/,
        use: 'raw-loader',
        include: path.resolve(__dirname, 'src'),
        exclude: /node_modules/,
      },
    ],
  },
}
  • exclude: Exclude all qualified files
  • include: Only all qualified files

Reasonable use of the include and exclude attributes can effectively reduce the file matching range, thereby reducing build time.

**Reference: ** webpack module.rules

File suffix matching

Usually when we import modules, we are accustomed to ignoring the file suffix name because webpack will help complete.

But this comes at a cost. Webpack internally tries to use built-in configuration, complete the suffix and find the file exists, and then try to load it until the file matches. This can incur additional I/O overhead.

On the one hand, you can modify the webpack configuration resolve.extensions, adjust the suffix completion rules, and control the priority of completion through sequence. Put the most commonly used file suffixes at the top and reduce non-essential suffix names.

module.exports = {
  resolve: {
    // Do not write configuration if not necessary.
    extensions: ['.tsx', '.ts', '.js'],
  },
}

On the other hand, when importing modules, try not to ignore the file suffix name.

Reference: webpack resolve.extensions

cache

Every time you start the build, if you need to recompile all files, it will inevitably take a long time. Therefore, the compilation results need to be cached so that the cached results will be loaded directly next time and only the modified files will be recompiled.

In webpack5, a cache configuration is provided to enable cache directly.

module.exports = {
  cache: {
    type: 'filesystem',
  },
}

Reference: webpack cache

Parallel construction

webpack runs in a NodeJS environment and is single-threaded, so it can only do one thing at a time. Currently, mainstream computers are multi-core, and this feature can be used to enable webpack to be built in parallel. Typically, use thread-loader to implement parallel construction.

module.exports = {
  module: {
    rules: [
      {
        test: /.jsx?$/,
        use: [
          // Enable multi-process packaging.
          {
            loader: 'thread-loader',
            options: {
              workers: 3, // 3 processes are opened
            },
          },
          { loader: 'babel-loader' },
        ],
      },
    ],
  },
}

The loader placed after the thread-loader will run in a separate worker pool. Each worker is a separate node.js process with a limit of 600ms. At the same time, data exchange across processes will also be restricted. So it is recommended to use it only on time-consuming loaders.

If the project is not large and there are not many files, there is no need to use thread-loader. It also has additional performance overhead.

Build product optimization

The purpose of building product optimization is to reduce the volume of the build product and organize the build product reasonably, thereby improving the loading speed of the page, the loading speed of the first screen, etc.

General build optimizations include:

  1. [Compress js, css, html code](#Compress -js-csshtml-code).
  2. [Compressed Image Resource] (#Compressed Image Resource).
  3. [Code segmentation] (#Code segmentation).
  4. [Load on demand] (#Load on demand).
  5. preload, prefetch.
  6. tree-shaking.

Compress js, css, html code

Compress js

Use terser-webpack-plugin to compress js code:

const TerserPlugin = require('terser-webpack-plugin')
module.exports = {
  optimization: {
    minimize: true,
    minimizer: [new TerserPlugin()],
  },
}

Compress css

Compress the css code through css-minimizer-webpack-plugin, Use mini-css-extract-plugin to extract css into a separate file.

const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          // Extract into separate files
          MiniCssExtractPlugin.loader,
          'css-loader',
        ],
      },
    ],
  },
  plugins: [
    new MiniCssExtractPlugin({
      // Define the output file name and directory
      filename: 'asset/css/style.css',
    }),
  ],
  optimization: {
    minimize: true,
    minimizer: [
      // Compress css
      new CssMinimizerPlugin({}),
    ],
  },
}

Compress html

Use html-webpack-plugin to compress html code.

const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
  plugins: [
    new HtmlWebpackPlugin({
      // Dynamically generate html files
      template: './index.html',
      minify: {
        // Compress HTML
        removeComments: true, // Remove comments in HTML
        collapseWhitespace: true, // Remove space and line breaks
        minifyCSS: true, // compress inline css
      },
    }),
  ],
}

Compress image resources

There are many ways to compress image resources and you need to choose according to actual conditions. It also includes processing of multiplex graphs, such as: @2x, @3x, @4x, etc.

For example, you can use image-webpack-loader to achieve image compression.

module.exports = {
  module: {
    rules: [
      {
        test: /\.(png|jpg|gif|jpeg|webp|svg)$/,
        use: [
          'file-loader',
          {
            loader: 'image-webpack-loader',
            options: {
              mozjpeg: { progressive: true },
              optipng: { enabled: false },
              pngquant: { quality: [0.65, 0.9], speed: 4 },
              gifsicle: { interlaced: false },
            },
          },
        ],
        exclude: /node_modules/,
      },
    ],
  },
}

Code segmentation

If it is a medium-sized project, or an MPA project, it will generally have multiple pages. But they all use the same technology stack and have reused public resources. If the code of each page contains these same codes alone, it will lead to waste of resources. Each time a different page is loaded, duplicate resources will be loaded. Waste of user traffic, slow page loading, affecting user experience.

In this case, the third-party modules and public resources are split into independent files separately. Using the caching mechanism, different pages only need to spend the first load time when loading, reducing the waiting time for secondary loading.

module.exports = {
  //...
  optimization: {
    splitChunks: {
      chunks: 'async', // The values ​​are `all`, `async` and `initial`
      minSize: 20000, // Generate the minimum volume of the chunk (in bytes).
      minRemainingSize: 0,
      minChunks: 1, // The minimum number of chunks that must be shared before splitting.
      maxAsyncRequests: 30, // Maximum number of parallel requests when loading on demand.
      maxInitialRequests: 30, // Maximum number of parallel requests for entry point.
      enforceSizeThreshold: 50000,
      cacheGroups: {
        defaultVendors: {
          test: /[\/]node_modules[\/]/, //The third-party module is removed
          priority: -10,
          reuseExistingChunk: true,
        },
        utilVendors: {
          test: /[\/]utils[\/]/, //Disassemble the public module
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
        },
      },
    },
  },
}

Reference: Code Separation

Load on demand

Most of the time, making the page available does not require loading all resources.

For example, in SPA/MPA applications, different pages are implemented through routing. If all page codes are packaged in the same file, Then when loading the code for a certain page, the code for other pages is actually loaded, resulting in the loading speed of the page not meeting expectations.

Because splitting the resources of the routing page into different files and loading these resources only when used can reduce the loading time of the current page.

const List = lazyComponent('list', () => import(/* webpackChunkName: "list" */ '@/pages/list'))
const Detail = lazyComponent('detail', () => import(/* webpackChunkName: "detail" */ '@/pages/detail'))

Furthermore, the faster the page is rendered, the more conducive it is to the user experience. Therefore, you can also analyze the key resources required to complete the first-screen rendering of the current page, split the non-critical resources, and only load it for the first time Critical resources, and then load non-critical resources after completion.

preload, prefetch

  • refetch: Resources that may be required under certain navigation in the future
  • preload (preload): Resources may be required under the current navigation

Use prefetch in webpack to implement prefetch:

// ...
import(/* webpackPrefetch: true */ './path/to/LoginModal.js')

This generates <link rel="prefetch" href="login-modal-chunk.js"> and appends to the page header, instructing the browser to prefetch the login-modal-chunk.js file at idle time.

Use preload in webpack to implement preload:

//...
import(/* webpackPreload: true */ 'ChartingLibrary')

When using ChartComponent in the page, while requesting ChartComponent.js, charting-library-chunk will also be requested via <link rel="preload">. Assume that the page-chunk volume is smaller and faster than the charting-library-chunk, The page will now display LoadingIndicator, wait until the charting-library-chunk request is completed. The LoadingIndicator component just disappeared. This will allow for a shorter loading time, as only a single round trip is performed, Instead of two round trips, especially in high latency environments.

Reference: webpack prefetch/preload

tree-shaking

tree shaking is turned on by default in production mode

What should be noted is:

  • Effective only for ESM
  • ES6 modules that can only be statically declared and referenced, and cannot be dynamically introduced and declared.
  • Only handle module level, but not function level redundancy.
  • Only JS-related redundant code can be processed, but not CSS redundant code.

For CSS resources, you can use the purgecss-webpack-plugin plugin to tree-shaking on CSS.

const PurgecssPlugin = require('purgecss-webpack-plugin')
module.exports = {
  plugins: [
    new PurgeCSSPlugin({
      paths: glob.sync('src/**/*', { nodir: true }),
    }),
  ],
}

Reference: webpack tree-shaking