The Webpack Deep Unlock Series (Basic) and The Webpack Deep Unlock Series (Advanced) are mainly about Webpack configuration, but as the project gets bigger, the building speed may get slower and the volume of js built will get bigger. This is where the Webpack configuration needs to be optimized.

This article lists more than a dozen optimization methods, you can combine their own projects, choose the appropriate way to optimize. I have not read most of the source code of these Webpack plug-ins, mainly combining Webpack official documents and project practices, and spend a lot of time to verify the output of this article, if there are mistakes in the article, please feel free to correct in the comment area.

Given the rapid change in front-end technology, this article is based on the Webpack version number:

├ ─ ─ [email protected] └ ─ ─ [email protected]Copy the code

The corresponding project address (used at the time of writing) is for reference: github.com/YvetteLau/w…

quantitative

Sometimes what we think of as optimization is negative optimization, and it would be nice to have a quantitative indicator to see before and after.

The speed-measurement-webpack-plugin allows you to measure the time spent by each plugin and loader. When you use the plugin, you will get information like the following:

The speed-measurement-webpack-plugin is easy to use and can be used to wrap webPack configurations directly:

//webpack.config.js
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();

const config = {
    / /... Webpack configuration
}

module.exports = smp.wrap(config);
Copy the code

1.exclude/include

We can use exclude and include configurations to ensure that as few files as possible are translated. As the name implies, exclude specifies the files to exclude, and include specifies the files to include.

Exclude takes precedence over include. Use absolute path arrays in both include and exclude. Avoid exclude in favor of include.

//webpack.config.js
const path = require('path');
module.exports = {
    / /...
    module: {
        rules: [{test: /\.js[x]? $/.use: ['babel-loader'].include: [path.resolve(__dirname, 'src')]}]},}Copy the code

Here is a comparison of my build results with and without include:

2. cache-loader

Add a cache-loader before some loaders with high performance overhead and cache the results to disks. The default directory is node_modueles/. Cache /cache-loader.

First install the dependencies:

npm install cache-loader -D
Copy the code

The cache-loader configuration is simple and can be placed in front of other loaders. Modify the Webpack configuration as follows:

module.exports = {
    / /...
    
    module: {
        // In my project,babel-loader takes a long time, so I configured 'cache-loader' for it
        rules: [
            {
                test: /\.jsx? $/.use: ['cache-loader'.'babel-loader'[}]}Copy the code

If, like me, you only want to configure cache for Babel-Loader, you can also add the cacheDirectory option to Babel-Loader without using cache-Loader.

CacheDirectory: The default is false. If yes, the specified directory is used to cache the execution results of the Loader. Later Webpack builds will attempt to read the cache to avoid a potentially high-performance, costly Babel recompile process each time it is executed. If null or true, use the default cache directory: node_modules/.cache/babel-loader. Enable the babel-Loader cache and configure the cache-loader, I compared, the build time is very close.

3.happypack

Because of the large number of files that need to be parsed and processed, the build is a file read/write and computation-intensive operation, especially as the number of files increases, Webpack’s slow build becomes a serious problem. While file reading and writing and computing operations are inevitable, can Webpack handle multiple tasks at once and harness the power of multi-core cpus to speed up build times?

HappyPack allows Webpack to do this by splitting tasks into multiple sub-processes for concurrent execution, which then send the results to the main process.

First you need to install happypack:

npm install happypack -D
Copy the code

Modify the configuration file:

const Happypack = require('happypack');
module.exports = {
    / /...
    module: {
        rules: [{test: /\.js[x]? $/.use: 'Happypack/loader? id=js'.include: [path.resolve(__dirname, 'src')]}, {test: /\.css$/.use: 'Happypack/loader? id=css'.include: [
                    path.resolve(__dirname, 'src'),
                    path.resolve(__dirname, 'node_modules'.'bootstrap'.'dist')]}]},plugins: [
        new Happypack({
            id: 'js'.// Corresponds to id=js in rule
            // Loaders from the previous rule are configured here
            use: ['babel-loader'] // Must be an array
        }),
        new Happypack({
            id: 'css'.// Id = CSS in the rule
            use: ['style-loader'.'css-loader'.'postcss-loader'],})]}Copy the code

The happypack starts with one CPU core by default. We can also pass threads to the happypack.

Note: When postcss-loader is configured in Happypack, you must create postcss.config.js in your project.

//postcss.config.js
module.exports = {
    plugins: [
        require('autoprefixer') (the)]}Copy the code

Otherwise, Error: Error: No PostCSS Config found is thrown

In addition, if your project is not very complex, you don’t need to configure Happypack, because it takes time to allocate and manage the process, and it doesn’t increase the build speed or even slow it down.

4.thread-loader

In addition to using Happypack, we can also use thread-loader, which is placed before other loaders, so that loaders placed after this loader will run in a separate worker pool.

The loader running in the worker pool is limited. Such as:

  • theseloaderUnable to generate new file.
  • theseloaderCannot use customloaderAPI (that is, through plug-ins).
  • theseloaderUnable to getwebpackOption Settings.

First install the dependencies:

npm install thread-loader -D
Copy the code

Modify the configuration:

module.exports = {
    module: {
        // In my project,babel-loader takes a long time, so I configure thread-loader for it
        rules: [
            {
                test: /\.jsx? $/.use: ['thread-loader'.'cache-loader'.'babel-loader'[}]}Copy the code

I compared thread-Loader with Happypack, and the build time is almost the same. However, thread-Loader is simple to configure.

5. Enable JS multi-process compression

Although many WebPack optimization articles will mention the optimization of multi-process compression, either webpack-parallel-Uglify-plugin or Uglifyjs-Webpack-plugin to configure parallel. However, there is no need to install these plug-ins separately and they will not make your Webpack build any faster.

Currently, Webpack uses the TerserWebpackPlugin by default. By default, multiple processes and caching are enabled. At build time, you can see the terser cache file node_modules/.cache/terser-webpack-plugin in your project.

6.HardSourceWebpackPlugin

HardSourceWebpackPlugin provides an intermediate cache for modules. The default path for storing the cache is node_modules/. Cache /hard-source.

With the hard-source-webpack-plugin, the first build time doesn’t change much, but the second time, the build time is saved by about 80%.

First install the dependencies:

npm install hard-source-webpack-plugin -D
Copy the code

Modify webPack configuration:

//webpack.config.js
var HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
module.exports = {
    / /...
    plugins: [
        new HardSourceWebpackPlugin()
    ]
}
Copy the code

In another test on a larger project, the HardSourceWebpackPlugin was configured and the build time was reduced from around 8S to around 2S.

The HardSourceWebpackPlugin documentation lists some of the problems you might encounter and how to fix them, such as hot updates failing or certain configurations not working.

7.noParse

If some third-party modules do not have AMD/CommonJS specification versions, you can use noParse to identify the modules so that Webpack will introduce them, but will not convert or parse them, thus improving Webpack’s build performance. For example: jquery, Lodash.

The value of the noParse property is either a regular expression or a function.

//webpack.config.js
module.exports = {
    / /...
    module: {
        noParse: /jquery|lodash/}}Copy the code

My current Webpack-optimize project doesn’t use jquery or LOdash.

So create a new project test, just introduce jquery and loadsh, and then build the comparison time with and without noParse configured.

Before configuring noParse, the build takes 2392ms. With noParse configured, the build takes 1613ms. If you are using third-party dependencies that do not need to be resolved, configuring noParse is an obvious optimization.

8.resolve

Resolve configures how WebPack finds the corresponding file for a module. Assuming we make sure that all modules are found in the root directory of node_modules, we can configure:

//webpack.config.js
const path = require('path');
module.exports = {
    / /...
    resolve: {
        modules: [path.resolve(__dirname, 'node_modules')].}}Copy the code

Keep in mind that if you configured resolve-. moudles above, you might have a problem. For example, if you have node_modules in your dependency, you might have a problem telling you that the corresponding file is there, but it can’t be found. Therefore, I personally do not recommend this configuration. If other colleagues are not familiar with this configuration, they will be left scratching their heads when confronted with this problem.

Also, the Resolve Extensions configuration, which defaults to [‘.js’, ‘.json’], if you want to configure it, remember to put the most frequent suffix first and control the length of the list to reduce the number of attempts.

This project is small, so the optimization effect here is not obvious during the test.

9.IgnorePlugin

Webpack – a built-in plugin that ignores third-party package directories.

For example, moment (version 2.24.0) packages all localized content along with core functionality, and we can use IgnorePlugin to ignore localized content when packaging.

//webpack.config.js
module.exports = {
    / /...
    plugins: [
        // Ignore the./locale directory under moment
        new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)
    ]
}
Copy the code

When using, if we need to specify the language, then we need to manually import the language pack. For example, import the Chinese language pack:

import moment from 'moment';
import 'moment/locale/zh-cn';// Manually import
Copy the code

Only moment is introduced in index.js, and the bundle.js package is 263KB. If IgnorePlugin is configured and moment/locale/zh-cn is introduced separately, the package size is 55KB.

10.externals

We can store some JS files in CDN (reduce the size of Webpack JS) and introduce them in index.html with

<! DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, Word-wrap: break-word! Important; "> <meta http-equiv=" x-uA-compatible "content="ie=edge"> <title>Document</title> <body> <div Id = "root" > root < / div > < script SRC = "http://libs.baidu.com/jquery/2.0.0/jquery.min.js" > < / script > < / body > < / HTML >Copy the code

We want to use externals in a way that can still be referenced by import (such as import $from ‘jquery’), and we don’t want WebPack to package it.

//webpack.config.js
module.exports = {
    / /...
    externals: {
        // Jquery is a global variable
        'jquery': 'jQuery'}}Copy the code

11.DllPlugin

Sometimes, if all JS files are printed into a single JS file, the resulting JS file will be very large. At this point, we should consider splitting bundles.

DllPlugin and DLLReferencePlugin are built-in webPack modules to split bundles and greatly improve the speed of building.

We use the DllPlugin to compile libraries that are not updated frequently, so there is no need to recompile when the versions of these dependencies do not change. For example: webpack.config.dll. Js. Here we pack react and React-dom as separate dynamic link libraries.

//webpack.config.dll.js
const webpack = require('webpack');
const path = require('path');

module.exports = {
    entry: {
        react: ['react'.'react-dom']},mode: 'production'.output: {
        filename: '[name].dll.[hash:6].js'.path: path.resolve(__dirname, 'dist'.'dll'),
        library: '[name]_dll' // Expose to external use
        //libraryTarget specifies how to expose content. By default, it is var
    },
    plugins: [
        new webpack.DllPlugin({
            // Name is the same as library
            name: '[name]_dll'.path: path.resolve(__dirname, 'dist'.'dll'.'manifest.json') // The manifest. Json generation path}})]Copy the code

In package.json scripts add:

{
    "scripts": {
        "dev": "NODE_ENV=development webpack-dev-server"."build": "NODE_ENV=production webpack"."build:dll": "webpack --config webpack.config.dll.js"}},Copy the code

When you run NPM run build:all, you can see that the dist directory is as follows. The main reason why you put dynamic link libraries in a separate DLL directory is to use CleanWebpackPlugin to filter out dynamic link libraries more easily.

Dist ├─ DLL ├── manifest. Json ├─ react.dll9.dcd9d.js
Copy the code

Manifest.json is used to map the DLLReferencePlugin to the related dependencies.

Modify webPack’s main configuration file: webpack.config.js:

//webpack.config.js
const webpack = require('webpack');
const path = require('path');
module.exports = {
    / /...
    devServer: {
        contentBase: path.resolve(__dirname, 'dist')},plugins: [
        new webpack.DllReferencePlugin({
            manifest: path.resolve(__dirname, 'dist'.'dll'.'manifest.json')}),new CleanWebpackPlugin({
            cleanOnceBeforeBuildPatterns: ['* * / *'.'! dll'.'! dll/**'] // Do not delete the DLL directory
        }),
        / /...]}Copy the code

Using NPM Run build, you can see that bundle.js is greatly reduced in size.

Modify the public/index. HTML file to introduce react.dll.js

<script src="/dll/react.dll.9dcd9d.js"></script>
Copy the code

Build speed

Package volume

12. Detach from common code

Extracting common code is useful for multi-page applications. If common modules are introduced into multiple pages, then these common modules can be detached and packaged separately. Common code only needs to be downloaded once and cached, avoiding repeated downloads.

Extracting common code should be configured no differently for single-page applications than for multi-page applications, both in optimity.splitchunks.

//webpack.config.js
module.exports = {
    optimization: {
        splitChunks: {// Split the code block
            cacheGroups: {
                vendor: {
                    // Third party dependencies
                    priority: 1.// Set the priority to remove the third-party module first
                    name: 'vendor'.test: /node_modules/.chunks: 'initial'.minSize: 0.minChunks: 1 // Introduce at least 1 time
                },
                / / cache group
                common: {
                    // Public module
                    chunks: 'initial'.name: 'common'.minSize: 100.// The size exceeds 100 bytes
                    minChunks: 3 // Introduce at least 3 times
                }
            }
        }
    }
}
Copy the code

Even single-page applications can use this configuration. For example, if the bundle.js package is too large, we can package some dependencies into dynamically linked libraries and then unpack the rest of the third-party dependencies. This effectively reduces the size of bundle.js. Of course, you can continue to extract the common module of business code, which is not configured here because there is less source code in my project.

runtimeChunk

RuntimeChunk is used to separate the chunk mapping list from main.js. When you configure splitChunk, configure runtimeChunk.

module.exports = {
    / /...
    optimization: {
        runtimeChunk: {
            name: 'manifest'}}}Copy the code

The resulting file will generate a manifest.js.

Further optimize with webpack-Bundle-Analyzer

When doing webPack build optimization, vendor print out over 1M, react and React-DOM have been packaged as DLLS.

So you need to use Webpack-bundle-Analyzer to see which packages are large.

First install the dependencies:

npm install webpack-bundle-analyzer -D
Copy the code

It is also easy to use, modify our configuration:

//webpack.config.prod.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const merge = require('webpack-merge');
const baseWebpackConfig = require('./webpack.config.base');
module.exports = merge(baseWebpackConfig, {
    //....
    plugins: [
        / /...
        new BundleAnalyzerPlugin(),
    ]
})
Copy the code

NPM run build will be opened by default: http://127.0.0.1:8888/, you can see the size of each package:

Further, the vendor is split into four chunks (splitChunks are used).

module.exports = {
    optimization: {
    concatenateModules: false.splitChunks: {// Split the code block
      maxInitialRequests:6.// The default is 5
      cacheGroups: {
        vendor: {
          // Third party dependencies
          priority: 1.name: 'vendor'.test: /node_modules/.chunks: 'initial'.minSize: 100.minChunks: 1 // Repeat the introduction several times
        },
        'lottie-web': {
          name: "lottie-web".// Unpack react-Lottie separately
          priority: 5.// The weight must be greater than 'vendor'
          test: /[\/]node_modules[\/]lottie-web[\/]/.chunks: 'initial'.minSize: 100.minChunks: 1 // Repeat the introduction several times
        },
        / /...}}},}Copy the code

Rebuild, and the result looks like this:

13. Optimization of WebPack itself

tree-shaking

If you use the IMPORT syntax of ES6, code that is not used is automatically removed in a production environment.

//math.js
const add = (a, b) = > {
    console.log('aaaaaa')
    return a + b;
}

const minus = (a, b) = > {
    console.log('bbbbbb')
    return a - b;
}

export {
    add,
    minus
}
Copy the code
//index.js
import {add, minus} from './math';
add(2.3);
Copy the code

The minus function is not packaged in the final code you build.

Scope Hosting scope upgraded

Variable promotion can reduce some of the variable declarations. In a production environment, this function is enabled by default.

In addition, we test the notice, speed measure – webpack – the plugin and HotModuleReplacementPlugin cannot be used at the same time, otherwise an error:

Babel configuration optimization

If you’re new to Babel, read this article: What you Can’t Miss about Babel7.

When @babel/ plugin-transform-Runtime is not configured, Babel uses small helper functions to implement public methods such as _createClass. By default, it will be injected into every file that needs it. However, the result of this is that the size of the JS built is larger.

We also don’t need to inject helper functions in every JS, so we can use @babel/ plugin-transform-Runtime. @babel/ plugin-transform-Runtime is a helper that can reuse Babel injection. To save code size plug-ins.

So we can add @babel/ plugin-transform-Runtime to.babelrc.

{
    "presets": []."plugins": [["@babel/plugin-transform-runtime"]]}Copy the code

Those are some of the optimizations I’ve used so far. If you have a better one, please leave a comment in the comments section. Thanks for reading.

After reading this article, it is time to go to the scaffolding: [advanced front end essential] hand touch hand teach you to hand a scaffolding

The last

Give this article a like if it helped you.

Follow public Account

Reference documents:

  • Some basic approaches to WebPack optimization
  • Module (Module)
  • IgnorePlugin
  • DllPlugin
  • Use Webpack’s DllPlugin to speed up project builds
  • Using HappyPack
  • Webapck4 extracts the public module “SplitChunksPlugin”
  • Hard-source-webpack-plugin, an alternative to webpack DllPlugin configuration