webpack: Detailed explanation of code separation and the use of the plug-in SplitChunksPlugin

background

Code separation can be said to be the most powerful feature of webpack. Using code separation, chunks can be separated into different bundles. For example, libraries that are not updated frequently can be packaged together and cached in a bundle, which can reduce loading time, etc. wait.

What are chunks?
Original English meaning: block. The module that can be referenced by import, require, etc. is chunk.

What is a bundle?
Original English meaning: bundle, bundle, package. A package that packages one or more modules into a whole is called a bundle, such as the content in the packaged dist in our project.

There are two commonly used code separation methods:

  • Entry point separation: manually separate code using entry configuration
  • SplitChunksPlugin plugin separation

Problems you may encounter:

  • Duplicate question

Code separation tips:

  • Dynamic imports: Separate code through inline function calls of modules.

https://webpack.docschina.org/guides/code-splitting/

Entry point separation

Basic usage

We configure two entrances

const path = require('path');

module.exports = {
    
    
 mode: 'development',
 entry: {
    
    
   index: './src/index.js',
   another: './src/another-module.js',
 },
  output: {
    
    
   filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
};

Among them, index.js introduces another-module.js, and another-module.js introduces lodash.
But after packaging we found two files:

index.bundle.js 553 KiB
another.bundle.js 553 KiB

That is to say, if there are some duplicate modules between the entry chunks, then these duplicate modules will be introduced into each bundle. Of course, this can also be solved. You only need to configure the dependOn option to prevent duplication.

Anti-duplication

const path = require('path');

module.exports = {
    
    
  mode: 'development',
  entry: {
    
    
   index: {
    
    
     import: './src/index.js',
     // add
     dependOn: 'shared',
   },
   another: {
    
    
     import: './src/another-module.js',
     // add
     dependOn: 'shared',
   },
   // add
   shared: 'lodash',
  },
  output: {
    
    
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
};

If you want to use multiple entries on an HTML page, you also need to set optimization.runtimeChunk: 'single'

const path = require('path');

module.exports = {
    
    
  mode: 'development',
  entry: {
    
    
   index: {
    
    
     import: './src/index.js',
     dependOn: 'shared',
   },
   another: {
    
    
     import: './src/another-module.js',
     dependOn: 'shared',
   },
   shared: 'lodash',
  },
  output: {
    
    
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
  // add
  optimization: {
    
    
    runtimeChunk: 'single',
  },
};
shared.bundle.js 549 KiB
runtime.bundle.js 7.79 KiB
index.bundle.js 1.77 KiB
another.bundle.js 1.65 KiB

The official does not recommend multiple entries. Even if it is multiple entries, entry: { page: ['./analytics', './app'] }this writing method is also recommended.

SplitChunksPlugin plugin separation

background

Initially, chunks (and internally imported modules) are related through parent-child relationships in the internal webpack graph. CommonsChunkPlugin was used to avoid duplicate dependencies between them, but no further optimization was possible.

Starting from webpack v4, CommonsChunkPlugin has been removed and replaced by optimization.splitChunks. So this plug-in is built into webpack and does not need to be imported separately.

Basic usage

If you don't configure it optimization.splitChunks, webpack will use this default configuration. The purpose of the configuration here is to indicate what kind of modules can be divided and packaged. For example, the following minSize indicates that only modules greater than or equal to 2k will be divided.

module.exports = {
    
    
  //...
  optimization: {
    
    
    splitChunks: {
    
    
      chunks: 'async',
      minSize: 20000,
      minRemainingSize: 0,
      minChunks: 1,
      maxAsyncRequests: 30,
      maxInitialRequests: 30,
      enforceSizeThreshold: 50000,
      cacheGroups: {
    
    
        defaultVendors: {
    
    
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
          reuseExistingChunk: true,
        },
        default: {
    
    
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
        },
      },
    },
  },
};

Let’s analyze the meaning of these fields:

  • By default, code splitting is only performed on modules imported on demand;
  • Modules from node_modules, or modules that are referenced twice or more, will be code split;
  • The divided module must be larger than 30kb (before code compression);
  • When loading on demand, the number of parallel requests must be less than or equal to 5;
  • When the initial page is loaded, the number of parallel requests must be less than or equal to 3;

Next, here are just some important field meanings:

splitChunks.chunks

Optional values:function (chunk) | initial | async | all

  • initialRepresents non-dynamically introduced modules in the entry file
  • allRepresents all modules
  • asyncRepresents a module introduced asynchronously

Dynamic/asynchronous import
The first one conforms to the import() syntax of the ECMAScript proposal
. The second one is a legacy feature of webpack and uses webpack-specific require.ensure

splitChunks.minChunks

The minimum number of chunks that a module must share before splitting, that is to say, if this module is dependent on several times before it will be split, the default is 1

splitChunks.minSize

The minimum size of generated chunk, unit is subsection, 1K=1024bytes

splitChunks.maxSize

Same as above and opposite

splitChunks.name

The user specifies the name of the split module. Set to true to automatically generate based on chunks and cacheGroup key.

Optional values:boolean: true | function (module, chunks, cacheGroupKey) | string

Names can be obtained in three ways

module.rawRequest
module.resourceResolveData.descriptionFileData.name
chunks.name

When using chunks.name to obtain, you need to use webpack's magic annotation.

import(/*webpackChunkName:"a"*/ './a.js')

Example:

name(module, chunks, cacheGroupKey) {
    
    
  // 打包到不同文件中了
  return `${
      
      cacheGroupKey}-${
      
      module.resourceResolveData.descriptionFileData.name}`;
  // 如果是写死一个字符串,那么多个chunk会被打包到同一个文件中,这样可能会导致首次加载变慢
  // return 'maincommon';
  // 指定打包后的文件所在的目录
  // return 'test/commons';
}
splitChunks.cacheGroups

Cache groups can inherit and/or override any options from splitChunks.*. But test, priority, and reuseExistingChunk can only be configured at the cache group level. Set them to false to disable any default cache groups.

module.exports = {
    
    
  //...
  optimization: {
    
    
    splitChunks: {
    
    
      cacheGroups: {
    
    
        // 默认为 true,表示继承 splitChunks.* 的字段
        default: false,
      },
    },
  },
};
splitChunks.cacheGroups.{cacheGroup}.priority

A module can belong to multiple cache groups, so priority is required. The priority of the default group is a negative number, and the priority of our custom group defaults to 0

splitChunks.cacheGroups.{cacheGroup}.reuseExistingChunk

If the chunk in this cache group already exists in the entry module (main module), it will not be introduced.

splitChunks.cacheGroups.{cacheGroup}.test
module.exports = {
    
    
  //...
  optimization: {
    
    
    splitChunks: {
    
    
      cacheGroups: {
    
    
        svgGroup: {
    
    
          test(module) {
    
    
            // `module.resource` contains the absolute path of the file on disk.
            // Note the usage of `path.sep` instead of / or \, for cross-platform compatibility.
            const path = require('path');
            return (
              module.resource &&
              module.resource.endsWith('.svg') &&
              module.resource.includes(`${
      
      path.sep}cacheable_svgs${
      
      path.sep}`)
            );
          },
        },
        byModuleTypeGroup: {
    
    
          test(module) {
    
    
            return module.type === 'javascript/auto';
          },
        },
        testGroup: {
    
    
		  // `[\\/]` 是作为跨平台兼容性的路径分隔符,也就是/
		  test: /[\\/]node_modules[\\/]/,
		}
      },
    },
  },
};
splitChunks.cacheGroups.{cacheGroup}.filename
module.exports = {
    
    
  //...
  optimization: {
    
    
    splitChunks: {
    
    
      cacheGroups: {
    
    
        defaultVendors: {
    
    
          filename: '[name].bundle.js',
          filename: (pathData) => {
    
    
            // Use pathData object for generating filename string based on your requirements
            return `${
      
      pathData.chunk.name}-bundle.js`;
          },
        },
      },
    },
  },
};

optimization.runtimeChunk

optimization: {
    
    
    runtimeChunk: 'single',
}
// 等同于
optimization: {
    
    
    runtimeChunk: {
    
    
		name: 'runtime'
	}
}

To optimize the persistent cache, runtime refers to the running environment of webpack (the specific function is module parsing and loading) and the module information list. The module information list will change every time there is a module change (hash change), so we want to Part of the code is packaged separately, in conjunction with the back-end caching strategy, so that the module containing module information (usually included in the last bundle) cache will not be invalidated due to changes in a module. optimization.runtimeChunk tells webpack whether to Pack this part separately.

Assume a case of using dynamic import (using import()), dynamically import component.js in app.js

const app = () =>import('./component').then();

After build, 3 packages are generated.

0.01e47fe5.js
main.xxx.js
runtime.xxx.js

The runtime is used to manage the separated packages. Below is a screenshot of runtimeChunk, where you can see the chunkId.

...
function jsonpScriptSrc(chunkId) {
    
    
/******/         return __webpack_require__.p + "" + ({
    
    }[chunkId]||chunkId) + "." + {
    
    "0":"01e47fe5"}[chunkId] + ".bundle.js"
/******/     }
...

If this subcontracting strategy is adopted

When the app is changed, the names (hash) of the runtime and (the separated dynamically loaded code) 0.01e47fe5.js will not change, but the name (hash) of main will change.
When component.js is changed, the name (hash) of main will not change, but the name (hash) of runtime and (dynamically loaded code) 0.01e47fe5.js will change.

Example

Put the default configuration here for comparison and easy reference.

module.exports = {
    
    
  //...
  optimization: {
    
    
    splitChunks: {
    
    
      chunks: 'async',
      minSize: 20000,
      minRemainingSize: 0,
      minChunks: 1,
      maxAsyncRequests: 30,
      maxInitialRequests: 30,
      enforceSizeThreshold: 50000,
      cacheGroups: {
    
    
        defaultVendors: {
    
    
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
          reuseExistingChunk: true,
        },
        default: {
    
    
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
        },
      },
    },
  },
};

Example 1

// 静态引入
import lodash from 'lodash'
import(/*webpackChunkName:"jquery"*/'jquery')
import('./echarts.js')
console.log('hello world')
module.exports = {
    
    
  //...
  optimization: {
    
    
    splitChunks: {
    
    
      chunks: 'async',
      name(module, chunks, cacheGroupKey) {
    
    
        // 打包到不同文件中了
        return `${
      
      cacheGroupKey}-${
      
      module.resourceResolveData.descriptionFileData.name}`;
      },
    },
  },
};

lodash is introduced statically, jquery and echarts are introduced dynamically, and we have configured async here, so the packaging will separate out the dynamically introduced packages:

// 可以看到这里的 cacheGroupKey 就是 defaultVendors,也就是默认的分组名称。
defaultVendors-jquery.js
// 主包中包含了lodash和console.log('你好')
main.js
// echarts 也是动态引入的,但是由于走的相对路径,所以name函数无法对其自定义,因为name函数是在外面,只对默认的 defaultVendors 组负责,而这个组中没有对 name 的自定义,所以就生成了默认的。
510.js

Example 2

So next we group echarts

module.exports = {
    
    
  //...
  optimization: {
    
    
    splitChunks: {
    
    
      chunks: 'async',
      name(module, chunks, cacheGroupKey) {
    
    
        // 打包到不同文件中了
        return `${
      
      cacheGroupKey}-${
      
      module.resourceResolveData.descriptionFileData.name}`;
      },
      cacheGroups: {
    
    
        echartsVendor: {
    
    
          test: /[\\/]echarts/,
          name: 'echarts-bundle',
          chunks: 'async',
        },
      },
    },
  },
};
// 可以看到这里的 cacheGroupKey 就是 defaultVendors,也就是默认的分组名称。
defaultVendors-jquery.js
// 主包中包含了lodash和console.log('你好')
main.js
// 由 echartsVendor 组生成的bundle
echarts-bundle.js

Example 3

When packaging a small program, if the main package depends on subcontracted js, the subcontracted code will be packaged into the bundle of the main package. SplitChunks is also used to make adjustments here.

It was originally like this. You can see that all the packages are entered in common.

module.exports = {
    
    
  //...
  optimization: {
    
    
    splitChunks: {
    
    
      chunks: 'all',
      name: 'common'
    },
  },
};

After modification

module.exports = {
    
    
  //...
  optimization: {
    
    
    splitChunks: {
    
    
      chunks: 'all',
      name: 'common',
      cacheGroups: {
    
    
        echartsVendor: {
    
    
          test: /[\\/]subpackage-echarts[\\/]/,
          name: 'subpackage-echarts/echartsVendor',
          chunks: 'all'
        },
        compontentsVendor: {
    
    
          test: /[\\/]subpackage-components[\\/]/,
          name: 'subpackage-components/componentsVendor',
          chunks: 'all',
          minSize: 0
        }
      }
    },
  },
};

As you can see, if subpackage-echarts and subpackage-components are subpackaged, I will put the packaged bundle into the corresponding subpackage folder. But I think this is a bit hard-coded. If you add another subcontract later, you will still have this problem, so I modified it again.

const {
    
     resolve } = require('path')
const fs = require('fs')
/**
 * @function 获取分包名称
 * @returns {Array} ['subpackage-a', 'subpackage-a']
 */
const getSubpackageNameList = () => {
    
    
  const configFile = resolve(__dirname, 'src/app.json')
  const content = fs.readFileSync(configFile, 'utf8')
  let config  = ''
  try {
    
    
    config = JSON.parse(content)
  } catch (error) {
    
    
    console.log(configFile)
  }

  const {
    
     subpackages } = config
  return subpackages.map(item => item.root)
}

module.exports = {
    
    
  //...
  optimization: {
    
    
    splitChunks: {
    
    
      chunks: 'all',
      name: 'common',
      cacheGroups: {
    
    
        subVendor: {
    
    
          test: (module) => {
    
    
            const list = getSubpackageNameList()
            const isSubpackage = list.some(item => module.resource.indexOf(`/${
      
      item}/`) !== -1)
            return isSubpackage
          },
          name(module, chunks, cacheGroupKey) {
    
    
            const list = getSubpackageNameList()
            const subpackageName = list.find(item => module.resource.indexOf(`/${
      
      item}/`) !== -1)
            return `${
      
      subpackageName}/vendor`
          },
          chunks: 'all',
          minSize: 0
        },
      }
    },
  },
};

Insert image description here

You can see that after packaging, all the files that depend on subpackaging are placed in the subpackage. In this way, no matter how you add subcontracting later, you don't need to modify the code.

Guess you like

Origin blog.csdn.net/weixin_43972437/article/details/133137500