In the previous article, full resolution of Webpack configuration introduced some basic uses of Loaders and plugins in Webpack. As loaders and plugins become more and more used, projects become more and more time-consuming. So this time we will continue to learn how to optimize the WebPack configuration to make our projects run faster and take less time.

This article will understand the optimized configuration of Webpack from the four aspects of narrowing the file search scope, reducing packaged files, caching and multi-process.

Narrow the file search

Webpack starts with the Entry Entry, parses the import module statements in the file, and recursively parses. Each time you encounter an imported syntax, you do two things:

  1. Find the location of the import module, for examplerequire('vue')Go to introduce/node_modules/vue/dist/vue.runtime.common.jsfile
  2. The imported module is parsed by the corresponding loader. For example, the imported JS is called babel-loader to convert the code

When a project has only a few files, the file parsing process takes a few hundred milliseconds, but as the project size increases, the file parsing becomes more and more time-consuming, so we use webPack configuration to narrow our search for modules

Optimizing loader Configuration

In the previous article, we introduced the use of include/exclude to include/exclude files in node_modules.

{
    rules: [{
        test: /\.js$/,
        use: {
            loader: 'babel-loader'
        },
        // exclude: /node_modules/,
        include: [path.resolve(__dirname, 'src')]}}]Copy the code

Include indicates the directories in which the babel-loader is required, and exclude indicates the directories in which the babel-loader is not required. This is because when third-party modules are introduced, many modules are already packaged and do not need to be processed, such as Vue, jQuery, etc. If include/exclude is not specified, the loader processes the package, increasing the packaging time.

Optimize the module.noparse configuration

If some third-party modules do not use the AMD/CommonJs specification, you can use noParse to mark the module, so that Webpack does not have to parse and transform the module when importing it, which increases the speed of Webpack construction. NoParse can accept either a regular expression or a function:

{
    module: {
        //noParse: /jquery|lodash|chartjs/,
        noParse: function(content){
            return /jquery|lodash|chartjs/.test(content)
        }
    }
}
Copy the code

Some libraries, such as jQuery, LoDash, ChartJS, etc., are large and do not adopt modular standards, so we can choose not to parse them.

Note: Module files that are not parsed should not contain module statements such as require and import

After several packaging attempts, the packaging performance can be improved by about 10%~20%; This example complete code demo,

Optimize the resolve.modules configuration

Modules is used to tell Webpack which directories to look for referenced modules. The default is [“node_modules”], which means to look for modules in./node_modules and then go to.. /node_modules, and so on.

There are also a large number of modules in our code that are dependent on and imported by other modules. Because these modules are not distributed in a fixed location, the path can sometimes be very long, such as import ‘.. /.. / SRC/components/button. ‘, the import. /.. / SRC/utils’; At this point we can use modules to optimize

{
  resolve: {
    modules: [
      path.resolve(__dirname, "src"),
      path.resolve(__dirname, "node_modules"),
      "node_modules",]}}Copy the code

This can be done simply by importing ‘components/button’, import ‘utils’, and webpack will find it from SRC first

Optimize the resolve.alias configuration

Alias Maps the original import module path to a new import path by creating an import or require alias. Resolve.modules is used to alias the preceding path instead of omitting it. The advantage of this is that WebPack directly looks for modules in the directory corresponding to the alias, reducing the search time.

{
  resolve: {
    alias: {
      The '@': path.resolve(__dirname, 'src'),}}},Copy the code

So we can import the component by importing Buttom from ‘@/Button’; We can alias not only our own modules, but also third-party modules:

{
  resolve: {
    alias: {
      'vue$': isDev ? 'vue/dist/vue.runtime.js' : 'vue/dist/vue.runtime.min.js',,}}}Copy the code

When we import Vue from ‘Vue’, Webpack will help us import the corresponding file under the Vue dependent package dist file, reducing the search time for package.json.

Optimize the resolve.mainfields configuration

MainFields tells WebPack which fields from a third-party module to use to import the module; A package.json file is used to describe the properties of the module, such as module name, version, auth, etc. One of the most important is that there are multiple special fields that tell WebPack where to import files. The reason there are multiple fields is that some modules can be used in multiple environments at the same time, and each environment can use a different file.

The default value for mainFields is related to the target property of the current WebPack configuration:

  • If the target iswebworkerorweb(Default), mainFields defaults to["browser", "module", "main"]
  • If target is anything else (including Node), mainFields defaults to["module", "main"]

This means that when we require(‘vue’), Webpack searches the browser field under vue first, then the Module field, and finally the main field.

In order to reduce the search steps, we can set this field as few as possible when specifying the third-party module entry file description field; Most third-party modules use the main field, so we can configure it like this:

{
    resolve: {
        mainFields: ["main"].}}Copy the code

Optimize the resolve.extensions configuration

The Extensions field is used to automatically insert suffixes when importing modules in an attempt to match the corresponding file. Its default value is:

{
    resolve: {
        extensions: ['.js'.'.json']}}Copy the code

When we require(‘./utils’), Webpack matches utils.js and then matches utils.json.

So the longer the Extensions array is, or the later the file with the correct suffix is, the more times it will take to match, so we can optimize it in the following ways:

  1. Keep the Extensions array to a minimum and don’t include file suffixes that don’t exist in the project
  2. File suffixes that occur more frequently are placed first
  3. When importing files into your code, try to include the suffix to avoid lookups

Above example complete code demo.

Reduce packaging

In our project, it is inevitable to introduce third-party modules. When webpack is packaged, third-party modules will also be packaged into bundles as dependencies, which will increase the package file size and time. If these modules can be handled reasonably, the performance of many Webpacks will be improved.

Extract common code

Our projects often have multiple pages or page modules (single pages), often with common functions or third-party modules in between. Packaging these modules in each page can cause the following problems:

  • Repeated resource loading wastes user traffic
  • Each page loads many resources, but the first screen display is slow

Before Webpack4, common code was extracted through the CommonsChunkPlugin, but there were the following problems

  • Generated chunks contain duplicate code when introduced
  • Unable to optimize asynchronous chunks

Webpack4 introduces SplitChunksPlugin plug-in to extract common modules. Due to the out-of-box nature of WebPack4, it does not need to be installed separately, but can be configured through optimization.splitchunks. The official default configuration parameters are as follows:

module.exports = {
  optimization: {
    splitChunks: {
      // Code split applies to asynchronous code by default, all: all code, inital: synchronous code
      chunks: 'async'.// The minimum module size for code splitting. Code splitting is performed only when the number of imported modules is greater than 20000B
      minSize: 20000.// The maximum size of a module for code splitting. If the size is greater than this, code splitting is performed. The default value is used
      maxSize: 0.// The number of introductions is greater than or equal to 1
      minChunks: 1.// The maximum number of asynchronous requests, i.e. the maximum number of modules loaded at the same time
      maxAsyncRequests: 30.// The entry file can be divided into up to 30 js files
      maxInitialRequests: 30.// The connection when the file is generated
      automaticNameDelimiter: '~'.enforceSizeThreshold: 5000.cacheGroups: {
        vendors: {
          // modules in node_modules do code splitting
          test: /[\\/]node_modules[\\/]/.// Decide which group to pack into according to the priority, such as a module in node_modules
          priority: -10 
        }, 
        // If both vendors and default are satisfied, they are packaged into vendors groups according to their priorities.
        default: { 
          // No test indicates that all modules can enter the default group, but note that it has a lower priority.
          // Decide which group to pack to according to the priority, pack to the group with the highest priority.
          priority: -20.// If a module has already been packaged, ignore the module when repackaged
          reuseExistingChunk: true}}}}};Copy the code

We introduce vue.js, axios.js and utility function module utils.js in home, list and detail respectively. We first extract the third-party modules we use into a separate file that contains the project’s underlying runtime environment, generally called vendor.js; After separating the third-party modules we extracted the common code that each page relied on and put it in common.js.

module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'initial'.cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: 10.name: 'vendors'
        },
        common: {
          test: /[\\/]src[\\/]/,
          priority: 5.name: 'common'
        }
      }
    }
  }
}
Copy the code

Sometimes projects rely on modules, and the vendors. Js file can be extremely large, and we can split it further by modules:


{
  // Omit other configurations
  cacheGroups: {
    // The module that involves vUE
    vue: {
      test: /[\\/]node_modules[\\/](vue|vuex|vue-router)/,
      priority: 10.name: 'vue'
    },
    // Other modules
    vendors: {
      test: /[\\/]node_modules[\\/]/,
      priority: 9.name: 'vendors'
    },
    common: {
      test: /[\\/]src[\\/]/,
      priority: 5.name: 'common'}}}Copy the code

Dynamically link the DllPlugin

DLL is the abbreviation of Dynamic Link Library, familiar with the Windows system in the computer can often see the suffix is DLL files, occasionally the computer pop-up warning is also because the computer is missing some DLL files; DLLS were originally used to save disk and memory space required by applications. When multiple programs use the same function library, DLLS can reduce the amount of code loaded in disk and memory repeatedly, which facilitates code reuse.

In Webpack also introduced the idea of DLL, we use the module out, packed into a separate dynamic link library, a dynamic link library can have more than one module; When we use a module across multiple pages, instead of repackaging, we import modules directly from the dynamically linked library.

Webpack integrates support for dynamic link libraries, mainly using two plug-ins:

  • DllPlugin: Creates a dynamic link library file
  • DllReferencePlugin: Introduces packaged dynamic link library files into the main configuration

We first use DllPlugin to create the dynamic link library file and create a new webpack.dll.js file under the project:

const path = require("path");
const webpack = require("webpack");

module.exports = {
  mode: "production".entry: {
    vue: ["vue"."vuex"."vue-router"].vendor: ["dayjs"."axios"."mint-ui"],},output: {
    path: path.resolve(__dirname, "public/vendor"),
    // Specify a file name
    filename: "[name].dll.js".// Expose the name of the global variable
    library: "[name]_dll_lib",},plugins: [
    new webpack.DllPlugin({
      path: path.join(__dirname, "public"."vendor"."[name].manifest.json"),
      name: "[name]_dll_lib",})]};Copy the code

Here entry sets multiple entry, each entry also has multiple module files; Then add the packaging command to package.json

{
    "scripts": {"build:dll": "webpack --config=webpack.dll.js"}}Copy the code

After executing NPM run build: DLL, we get our packaged DLL file in /public/vendor:

├── Vue.manifest.js ├─ vue.manifest.js ├─ vue.manifest.js ├─ vue.manifest.js ├─ vue.manifest.js ├─ vue.manifest.jsonCopy the code

The resulting package file happens to be named with two entry names. Take vue as an example. Look at the contents of vue.dll.js:

var vue_dll_lib =
/ * * * * * * / (function(modules) { 
    // Omit the webpackBootstrap code
/ * * * * * * / })
/ * * * * * * / ({

/ * * * / "./node_modules/vue-router/dist/vue-router.esm.js":
/ * * * / (function(module.exports, __webpack_require__) {
    // Omit vue-router module code
/ * * * / }),

/ * * * / "./node_modules/vue/dist/vue.runtime.esm.js":
/ * * * / (function(module.exports, __webpack_require__) {
    // Omit vue module code
/ * * * / }),

/ * * * / "./node_modules/vuex/dist/vuex.esm.js":
/ * * * / (function(module.exports, __webpack_require__) {
    // omit vuEX module code
/ * * * / }),

/ * * * * * * / });
Copy the code

As you can see, the dynamic link library contains all the code imported into the module, which is stored in an object and referenced by the module path as the key name; And exposed globally via vue_dll_lib; Vue. Manifest.json is used to describe which modules are included in the dynamic link library file:

{
    "name": "vue_dll_lib"."content": {
        "./node_modules/vue-router/dist/vue-router.esm.js": {
            "id": "./node_modules/vue-router/dist/vue-router.esm.js"."buildMeta": {}},"./node_modules/vue/dist/vue.runtime.esm.js": {
            "id": "./node_modules/vue/dist/vue.runtime.esm.js"."buildMeta": {}},"./node_modules/vuex/dist/vuex.esm.js": {
            "id": "./node_modules/vuex/dist/vuex.esm.js"."buildMeta": {}},}}Copy the code

Manifest.json describes which modules are contained in the corresponding JS file, and the key name (ID) of the corresponding module, so that we can introduce the dynamic link library as an external link in the template page. When Webpack resolves to the corresponding module, the module is obtained through the global variable:

<! -- public/index.html -->
<! DOCTYPEhtml>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
    <title>Document</title>
</head>
<body>
    <div id="app"></div>
    <! Dynamic link library -->
    <script src="./vendor/vendor.dll.js"></script>
    <script src="./vendor/vue.dll.js"></script>
</body>
</html>
Copy the code

Finally, we introduced the dynamic link library into the main configuration with the DllReferencePlugin at packaging time:

//webpack.config.js
{
    plugins: [
        new webpack.DllReferencePlugin({
            context: path.join(__dirname),
            manifest: require('./public/vendor/vendor.manifest.json')}),new webpack.DllReferencePlugin({
            context: path.join(__dirname),
            manifest: require('./public/vendor/vue.manifest.json')})]}Copy the code

Note: Dynamic link library package to /public/vendor directory, also need to copy it to the generated directory through CopyWebpackPlugin, otherwise there will be reference failure error; Packaging the dynamic link library file only needs to be done once, unless a module is later upgraded or new modules are introduced.

The introduction of the dynamic link library can put some modules in the project that are not updated frequently into external files. When we packaged the page logic code again, we found that the build speed was greatly improved, about 30%~40%. The relevant code is in Demo10.

externals

When we package the project, some third-party libraries will be introduced from the CDN (such as jQuery, etc.). If it is too bloated to package the project again in the bundle, we can configure externals to exclude these libraries from the package.

{
  externals: {
    'jquery': "jQuery".'react': 'React'.'react-dom': 'ReactDOM'.'vue': 'Vue'}}Copy the code

This means that when we encounter require(‘jquery’), we reference jquery from the global variable, and the other packages do the same; This removes jquery, React, Vue, and React-DOM from the bundle.

Tree Shaking

Tree Shaking was first implemented by rollup and later by WebPack2. Tree Shaking means Shaking a Tree. A Tree may have some leaves hanging from it, but it may already be dead.

The same is true for our project. We don’t use all the modules of the file, but WebPack packs the entire file, and the code that is never used in the file is “dead code”; In this case, Tree Shaking helps us weed out code modules that we don’t need.

For example, we define an utils.js file to export a number of utility modules, and then reference only certain modules in index.js:

//utils.js
var toString = Object.prototype.toString;

export function isArray(val) {
  return toString.call(val) === '[object Array]';
}
export function isFunction(val) {
  return toString.call(val) === '[object Function]';
}
export function isDate(val) {
  return toString.call(val) === '[object Date]';
}
//index.js
import { isArray } from './utils'
isArray()
Copy the code

We want to package only isArray functions into the bundle in our code; It is important to note that in order for Tree Shaking to work, we need to use the ES6 modular syntax, because the ES6 module syntax is statically loaded modules and has the following characteristics:

  1. Static loading of modules is more efficient than CommonJS module loading
  2. ES6 modules are loaded at compile time, making static analysis possible to further broaden JS syntax

If require, there is no way to analyze whether the module is available or not at run time, only at compile time, without affecting the state of the runtime.

Another problem with ES6 modules is that our code is usually compiled using Babel and Babel preset compiles any module type to Commonjs by default, so we need to fix this.

{
  "presets": [["@babel/preset-env",
      {
        // Add modules: false
        "modules": false}}]]Copy the code

After configuring Babel we need to have WebPack mark the “dead code” first:

{
  // Other configurations
  optimization: {
    usedExports: true.sideEffects: true,}}Copy the code

When we open the output bundle after running the package command, we find that some “dead code” still exists, but it has the unused Harmony export symbol added to it

/* unused harmony export isFunction */
/* unused harmony export isDate */
var toString = Object.prototype.toString;
function isFunction(val) {
  return toString.call(val) === '[object Function]';
}
function isDate(val) {
  return toString.call(val) === '[object Date]';
}
Copy the code

Although WebPack shows us which functions are unusable, we need to remove them through plug-ins; Since uglifyjs-webpack-plugin does not support ES6 syntax, we use terser-webpack-plugin instead:

const TerserJSPlugin = require("terser-webpack-plugin");
module.exports = {
  optimization: {
    usedExports: true.sideEffects: true.minimize: true.minimizer: [
      new TerserJSPlugin({
        cache: true.parallel: true.sourceMap: false,}),],}}Copy the code

This way we find that the packaged file has no extra code.

Note: Tree Shaking is enabled by default in production

We can also implement Tree Shaking for some of our common third-party modules; In the case of LoDash, there are a lot of functions in the package, but not all of them are the ones we use, so we need to strip out the ones we don’t use.

//index.js
import { chunk } from 'lodash'
console.log(chunk([1.2.3.4].2))
Copy the code

The size of the package can still reach 70+ KB, which should not be so large if only chunk is referenced. /node_modules/lodash/index.js /node_modules/lodash/index.js /node_modules/lodash/index.js /node_modules/lodash/index.js /node_modules/lodash/index.js NPM I-S Lodash-es: NPM i-S Lodash-es

//index.js
import { chunk } from 'lodash-es'
console.log(chunk([1.2.3.4].2))
Copy the code

So we’re generating a much smaller bundle; This example complete code demo.

The cache

We know that WebPack will call different Loaders to parse different files, and the process of parsing is also the most performance-consuming process. We only change a few files in the project at a time, and most files in the project don’t change that often; So if we cache the result of parsing the file, the next time we find the same file, we just need to read the cache, which can greatly improve parsing performance.

cache-loader

Cache-loader can cache some results produced by loaders that consume a lot of performance to disks. If the same code is used in the next package, the cache can be directly read to reduce performance consumption.

Note: Saving and reading caches also incur additional performance overhead, so cache-loader is suitable for loaders that consume a lot of performance, otherwise it will increase performance overhead

Cache-loader is also very simple to use. After installing the cache-loader, add it before the loader that needs to cache. For example, add cache to babel-loader:

{
  // omit other code
  rules: [{test: /\.js/,
      use: [
        {
          loader: 'cache-loader'
        },
        {
          loader: "babel-loader",},],},}Copy the code

However, we found that the speed of packing for the first time did not change significantly, and may even be slower than the original packing; /node_modules/. Cache /cache-loader/ As we continue to pack, the chart below shows how long it took me to pack several times:

We found that the first time we packed the files was about the same, but the second time we started caching the files made a big difference, reducing the time by 75%.

In addition to using cache-loader, babel-loader also provides caching functionality, configured via cacheDirectory:

{
  rules: [{test: /\.js/,
      use: [
        {
          loader: "babel-loader".options: {
            cacheDirectory: true},],},],}Copy the code

There are also more cache files in /node_modules/. Cache /babel-loader. After the comparison of two application results, cache-loader’s performance is better. This example complete code demo.

HardSourceWebpackPlugin

HardSourceWebpackPlugin can also provide caching for modules, agree to also cache files on disk

NPM i-d hard-source-webpack-plugin to install the plugin and add the plugin to the configuration:

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

The default HardSourceWebpackPlugin cache is /node_modules/. Cache /hard-source/[hash]. We can set its cache directory and when to create new cache hashes.

module.exports = {
  plugins: [
    new HardSourceWebpackPlugin({
      // Set the path to the cache directory
      // Relative path or absolute path
      cacheDirectory: 'node_modules/.cache/hard-source/[confighash]'.// Build different cache directory names
      // That is the [Confighash] value in cacheDirectory
      configHash: function(webpackConfig) {
        return require('node-object-hash') ({sort: false}).hash(webpackConfig);
      },
      / / hash environment
      // Replace caching when loader, plugin, or other NPM dependencies change
      environmentHash: {
        root: process.cwd(),
        directories: [].files: ['package-lock.json'.'yarn.lock'],},// Automatically clear the cache
      cachePrune: {
        // Maximum cache time (default: 2 days)
        maxAge: 2 * 24 * 60 * 60 * 1000.// All cache sizes exceeding size will be cleared
        / / 50 MB by default
        sizeThreshold: 50 * 1024 * 1024}})]}Copy the code

By trying to pack multiple times, I found that I saved about 90% of my time. This example complete code demo.

Multiple processes

As we mentioned in the event loop, JS is a single-threaded language, with only one thread processing tasks on the same event line; Therefore, when WebPack parses to JS, CSS, images or font files, it needs to parse and compile one by one, and cannot handle multiple tasks at the same time. Plug-ins can be used to divide tasks into multiple sub-processes for concurrent execution, and then send the results to the main process after the sub-process is finished.

happypack

Happypack automatically breaks down tasks and manages processes for us, and as the name suggests, it’s a happy plugin.

NPM i-D happypack can be configured in webpack:

const happypack = require("happypack");
module.exports = {
  module: {
    rules: [{test: /\.js/,
        exclude: /node_modules/.// Process the js file to the happypack instance with id js
        use: "happypack/loader? id=js",}],},plugins: [
    // What file does happypack process by id
    new happypack({
      id: "js".// Invoke the loader that processes the file, as described in rules
      loaders: [{
          loader: "babel-loader"}, {loader: "eslint-loader",},],},}Copy the code

Happypack will handle all rules/loader processing, and call the specific instance by ID, and then configure the specific loader in the instance to handle; In addition to id and loaders, we can configure the number of processes in the happypack instance:

// Share the process pool, which contains 5 sub-processes
var happyThreadPool = happypack.ThreadPool({
  size: 5
});
{
  plugins: [
    new happypack({
      id: "js".// Start several child processes. Default is 3
      threads: 3.// Share the process pool
      threadPool: happyThreadPool,
      // Whether to allow HappyPack to output logs
      verbose: true.loaders: [{
          loader: "babel-loader"}, {loader: "eslint-loader",},],},}Copy the code

Note: Only one of the Threads and threadPool fields needs to be configured.

We create a shared process pool with happypack.ThreadPool that contains five child processes. Each happypack instance can use the shared process pool to process files. Instead of assigning processes to each happypack instance, this prevents too many useless processes; Let’s pack and see how long it takes:

We’ve also found that happypack takes 20% to 30% more time than happypack.

Because our project is not large enough and loading multiple processes takes time and performance, happypack increases the time. So happypack is generally good for large projects; This example complete code demo.

thread-loader

If thread-loader is placed in front of other loaders, subsequent loaders will run in a separate process pool, but loaders running in the process pool have the following restrictions:

  • These loaders cannot generate new files.
  • These Loaders cannot use custom Loader apis (that is, through plug-ins).
  • These loaders cannot get the webpack option Settings.

So, that is to say as MiniCssExtractPlugin. Loader such as extraction of CSS loader is cannot use the thread – loader; Like Happypack, it is only suitable for large projects with lots of files:

module.exports = {
  module: {
    rules: [{test: /\.js$/,
        use: [
          "thread-loader"."babel-loader"}]}}Copy the code

This example complete code demo.

Recommended by author:

Full Webpack Configuration parsing (Basics)

Write promises from scratch

I write this summary after interviewing 50 people

For more front-end information, please pay attention to the public number [front-end reading].

If you think it’s good, check out my Nuggets page. Please visit Xie xiaofei’s blog for more articles