The Webpack documentation provides some suggestions for optimizing build performance – WebPack – Build performance, which can be useful for small projects!

Optimize module resolution rules

Rule. The include and rule. Exclude

When using loader, pass in necessary paths and files through include or exclude attributes to avoid global matching and improve the speed of WebPack construction.

For example, babel-loader ignores modules inside node_modules

module.exports = {
  module: {
    rules: [{test: /\.m? jsx? $/,
        exclude: /(node_modules)/./ / remove node_modules
        loader: 'babel-loader'.options: {
          presets: [['@babel/preset-env',
              {
                modules: false,}], ['@babel/preset-react']],plugins: [
            '@babel/plugin-proposal-class-properties',
            isDevelopment && require.resolve('react-refresh/babel'),
          ].filter(Boolean),},},],},};Copy the code

When matching image files, specify a specific folder:

module.exports = {
  module: {
    rules: [{test: [/\.bmp$/./\.gif$/./\.jpe?g$/./\.png$/./\.svg$/i].include: path.resolve(__dirname, 'src/assets/images'), // Contains only images folder
        use: [{loader: 'url-loader'.options: {
              limit: 10 * 1024.name: 'static/images/[name].[contenthash:8].[ext]',}}, {loader: 'image-webpack-loader'.options: {
              disable: isDevelopment,
            },
          },
        ],
      },
    ],
  },
};
Copy the code

resolve.modules

Resolve. modules Specifies the directory that webpack should search for when parsing modules. The default is [‘node_modules’]. /node_modules. In fact, this lookup is not necessary because projects don’t usually use nested layers.

Resolve can be used to construct absolute path values so that only modules are found in a given directory. Also, sometimes this configuration is simpler than alias, such as for a project where the encapsulated generic business component library is located under SRC/Components and resolved by the resolve.modules configuration module as follows

module.exports = {
  resolve: {
    modules: [
      path.resolve(__dirname, './src/components'),
      path.resolve(__dirname, './src'),
      path.resolve(__dirname, 'node_modules'),]}};Copy the code

When writing a page, if you import a component from SRC /components, you can write the folder directly without any path prefix. Nice! The downside is that if your component has the same name as a third-party library component, it will overwrite the third-party library component.

import { Button } from 'Button/index.jsx';
Copy the code

This configuration, combined with the aforementioned rules.resolve. extensions, is a handy tool for SRC /components components such as SRC /components/Button. Webpack automatically looks for the index file in the SRC/Components /Button folder.

import { Button } from 'Button';
Copy the code

resolve.symlinks

When developing several interdependent libraries, it is common to refer to each other in a symlink fashion. For a single project, it is good to disable resolve.symlinks directly.

module.exports = {
  resolve: {
    symlinks: false,}};Copy the code

module.noParse

Module. noParse specifies a string or regular expression. Webpack does not parse matching module names. This can improve build performance when large libraries are ignored.

module.exports = {
  module: {
    noParse: /jquery|lodash/,}};Copy the code

Using the cache

The right use of caches can speed up the second build of WebPack, and while the first use of these loaders or plugins may take a long time, subsequent implementation can lead to rapid improvements.

Optimization of Babel – loader

Babel-loader is the most important loader, it needs to process a large number of JS, JSX and other JS modules, through its built-in cache configuration to cache its execution results.

Babel-loader supports the following cache-related configuration items:

  • cacheDirectoryDefault is:false, when set totrueLater,babel-loaderThe default cache directory is usednode_modules/.cache/babel-loaderThe cache loader will try to read the cache in the future when webpack is packaged to avoid recompiling every time
  • cacheIdentifier: Specifies the cache identifier. The default value is by@babel/coreThe version number,babel-loaderThe version number,.babelrcFile contents (if present), environment variablesBABEL_ENVIs degraded toNODE_ENV) to form a string by changingcacheIdentifierInvalidate the cache
  • cacheCompressionDefault is:true, uses Gzip to compress the results of each Babel compilation
module.exports = {
  module: {
    rules: [{test: /\.m? jsx? $/,
        loader: 'babel-loader'.options: {
          / /...
          cacheDirectory: true,},},],},};Copy the code

Optimize eslint – loader

If your project is using eslint-Loader, be sure to use exclude to ignore the node_modules folder and enable the cache:true option, because eslint-Loader can take a lot of time to check your code!

In the past, it took about 10 minutes for eslint-Loader to pack, but it took 1/5 of the time for eslint-Loader to pack when caching was disabled!

cache-loader

Cache-loader is a Webpack loader that caches the execution results of the Loader to a local disk folder or a database. However, caching to the local folder itself involves IO operations, which can cause a larger memory footprint during webpack execution. Therefore, cache-loader is only used for loaders that need to compile a large number of modules.

  • babel-loader: Requires a lot of JS code to compile, but as mentioned above, it comes with caching options by default
  • css-loader: May be needed; Other similar loaders that handle CSS preprocessors are also available
  • image-webpack-loader: Compresses the image loader. This takes a lot of time. You can consider caching the execution results

Configuration items

The Cache-loader hasn’t been updated in nearly a year, but it’s maintained by the WebPack team, so it’s reliable.

Configuration items type The default value meaning
cacheContext {String} undefined Generate a cache relative to the set path
cacheKey {Function(options, request) -> {String}} undefined Overrides the function that generates the key of the cached item
cacheDirectory {String} findCacheDir({ name: 'cache-loader' }) or os.tmpdir() Sets the directory to which the cache writes and reads
cacheIdentifier {String} cache-loader:{version} {process.env.NODE_ENV} Sets an identifier to generate the hash
compare {Function(stats, dep) -> {Boolean}} undefined Modify the cache comparison function if returnedtrueUse the cache instead of executing the loader to generate new resources
precision {Number} 0 Round mtime by this number of milliseconds both for stats and dep before passing those params to the comparing function
read {Function(cacheKey, callback) -> {void}} undefined Use the function to generate a new content to override the cached content
readOnly {Boolean} false If you don’t want to update the cache and just read it, you can set this configuration item totrue
write {Function(cacheKey, data, callback) -> {void}} undefined Use functions to generate new resources to replace cached content

use

yarn add cache-loader -D
Copy the code

Add cache-loader in front of the loader that takes a long time. You can use the speed-measure-webpack-plugin to analyze the loader execution time.

Image-webpack-loader: image-webpack-loader: image-webpack-loader: image-webpack-loader: image-webpack-loader: image-webpack-loader: image-webpack-loader: image-webpack-loader I tested using cache-loader before urL-loader to lose images.

module.exports = {
  module: {
    rules: [{test: [/\.bmp$/./\.gif$/./\.jpe?g$/./\.png$/./\.svg$/i].include: path.resolve(__dirname, 'src/assets/images'),
        use: [{loader: 'url-loader'.options: {
              limit: 10 * 1024.//10KB
              name: 'static/images/[name].[contenthash:8].[ext]',}},'cache-loader'./ / into the cache - loader
          {
            loader: 'image-webpack-loader'.options: {
              disable: isDevelopment, // Disable compressed images in development environment},},],},],},};Copy the code

Before the introduction of cache-loader, my page contained a 7MB image that needed to be compressed, and the packaging time was as follows:

The processing time of image-webpack-loader is significantly shortened by using cache-loader to package the image again.

Multithreaded packaging

thread-loader

Thread -loader is a webpack loader provided by webpack team. If you are using Happypack, it is recommended to migrate to Thread -loader. Happypack is no longer maintained.

Threadloader uses the NodeJS worker pool mechanism, or thread pool. When the WebPack wrapper is started using Node, the webPack main runs on the main thread of the event loop, and the worker Pool handles the high-cost tasks.

Configuration items

Configuration items type meaning
workers Number Number of workers generated, default isNumber of CPU cores - 1
workerParallelJobs Number The number of parallel jobs executed in a worker process; The default is20
workerNodeArgs Array Additional Node.js parameters, for example['--max-old-space-size=1024']
poolRespawn Boolean [Fixed] Reopening a dead work pool Reopening will slow down the entire compilation, and the development environment should be set tofalse
poolTimeout Number Set to automatically terminate the worker after a certain period of time500ms, can be set toInfinity, which will keep the worker active
poolParallelJobs Number The number of jobs assigned to the worker by the pool, the default is200Reduction will reduce efficiency but make distribution more reasonable
name String The name of the worker pool that can be used to create different worker pools with other identical options

Max-old-space-size Specifies the additional nodejs parameter, described in the nodejs documentation. This CLI parameter specifies the maximum amount of system memory that the V8 engine can use for JS execution. The unit is MB. When nodeJS is running, V8 will have to perform GC frequently to free up unused variables if the memory limit is too low, and sometimes V8 will simply terminate the program when it needs too much memory. Node.js recommended “max-old-space-size” is a related problem on Stack Overflow.

The same goes for the Webpack wrapper, which is terminated when the memory limit is exceeded.

The solution to this problem is to specify CLI parameters in package.json nPm-script, for example:

  "scripts": {
    "build": "node --max-old-space-size=8192 scripts/build.js"
  },
Copy the code

use

Add thread-loaders before the configuration of other loaders and they will run in a worker pool. Each worker is an independent Node.js process. Starting the worker itself will generate extra overhead, and each worker will generate a delay of more than 600ms.

There are also restrictions on loaders executed using thread pools:

  • The loader cannot generate new files
  • Loader cannot use the customized Loader API
  • Loader cannot obtain the Webpack configuration
yarn add thread-loader -D
Copy the code
module.exports = {
  module: {
    rules: [{test: /\.js$/,
        include: path.resolve('src'),
        use: [
          'thread-loader'.'babel-loader'.// use before babel-loader],},],},};Copy the code

To avoid too long time before starting the worker, you can preheat the worker pool. For example, for the loader to be used, load it into the node.js module cache in advance.

const threadLoader = require('thread-loader');

threadLoader.warmup(
  {
    // Worker configuration items, which can be passed to the Loader},// loader
    'babel-loader'.'sass-loader',,);Copy the code

After I added thread-loaders to babel-loader and eslint-loader and enabled thread preheating, it did reduce the execution time of the relevant loaders by about 10 seconds, but overall the impact was not significant.

DLL

The term DLL (dynamic-link Library) comes from Microsoft’s packaging technology. In fact, DLL is a bit similar to the meaning of loading on demand, some shared code into A DLL, when the executable file calls the DLL function, the operating system will load the DLL file into memory.

However, there is no such thing as A DLL in JS. DllPlugin is just a way to pack some third-party libraries in advance to form a library, because they are rarely updated in a project. This way, WebPack takes more responsibility for packaging the project code. Greatly speed up construction. The meaning of code splitting is similar to code splitting, but code splitting still means that webpack should be packed every time a Webpack is built, which does not speed up the construction of webpack.

library

Library is a JS library. Lodash, for example, belongs to a JS library. Webpack provides processing for packing JS libraries. In fact, DLLS extract project code into a library by packaging the library, and expose the library to other modules in the project to use the library.

DllPlugin

DllPlugin is a built-in Webpack plugin responsible for pulling code out and packaging it separately. To use the DllPlugin, you need to create a new WebPack configuration file that handles packaging of third-party libraries.

Configuration items

Configuration items type If required meaning
context String no The context requested in the manifest file; The default is WebpackedcontextConfiguration items, i.ewebpack.config.jsThe current directory in which you reside
format Boolean no Format or notmanifest.json; The default isfalse
name String yes The function name of the exposed DLL
path String yes The output of themanifest.jsonAbsolute path of
entryOnly Boolean no The default istrueOnly the entrance is exposed
type String no The type of the generated DLL bundle

use

Now try to configure DllPlugin. Extract the React library from the packaging process, create a new webpack.dll.config.js configuration file in the project root directory, and use DllPlugin.

const path = require('path');
const webpack = require('webpack');
const { CleanWebpackPlugin } = require('clean-webpack-plugin'); // Clean up the build folder

module.exports = {
  mode: 'production'.entry: {
    react: ['react'.'react-dom'],},output: {
    path: path.resolve(__dirname, 'dll'),
    filename: '[name].[contenthash].dll.js'.library: '_[name]_dll',},plugins: [
    new CleanWebpackPlugin(),
    new webpack.DllPlugin({
      context: __dirname,
      path: path.resolve(__dirname, 'dll/[name]-manifest.json'),
      name: '_[name]_dll',})]};Copy the code

There are a few things to note about this configuration:

  • entryNeed to use the configuration form of the object, andThe value of each attribute must be an array, even if it contains only a single module
  • output.libraryDo not use the same name as the original module, preferably with a prefix or suffix, e.greactThe name of the module itself exposed isreactThen it can no longer be used herereact
  • output.libraryThe library is exposed as mentioned above so that other modules can use itoutput.libraryLink to extracted “DLL”,The property value must be equal toDllPluginThe inside of thenameThe configuration items must be consistent

Finally, write the command to execute the separate webpack.dll.config.js in package.json with npm-scripts.

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

Now executing the YARN DLL on the console will generate the packaged DLL files in the DLL folder at the root of the project, along with manifest.json files for the DllReferencePlugin.

React.dll. Xx.js and the react-dom.production.min.js production versions are included. Json file, internal contains the module ID, DLL name, DLL contains all modules.

DllReferencePlugin

The DllReferencePlugin is responsible for linking project chunks and DLLS together according to manifest.json generated by DllPlugin.

Configuration items

Configuration items type If required meaning
context String yes manifest.jsonThe requested context in the file; The default is WebpackedcontextConfiguration items, i.ewebpack.config.jsThe current directory in which you reside
scope String no The prefix of the content in a DLL
extensions Array no An extension used to resolve modules in a DLL bundlescopeThe use of
content String no Request mapping to module ID, default ismanifest.jsonin-filecontent
name String no The function name of the exposed DLL, default ismanifest.jsonin-filename
manifest Object yes String
sourceType String no How a DLL exposes its own modules, seeoutput.libraryTarget

Note: The context configuration item is mandatory and must point to the absolute path of the manifest.json directory. The webpack document describes the context in a vague example, using the use-dll-without-scope example to see how to use the context.

use

DllReferencePlugin is relatively simple to use and can be introduced as plugin in the original webpack.config.js of the project. If you have multiple extracted DLLS, you can use them multiple times.

module.exports = {
  plugin: [
    new webpack.DllReferencePlugin({
      context: path.resolve(__dirname, './dll'),
      manifest: require('./dll/react-manifest.json'),}),new webpack.DllReferencePlugin({
      context: path.resolve(__dirname, './dll'),
      manifest: require('./dll/other-manifest.json'),}),/ /...]};Copy the code

After yarn Build is executed, the Webpack automatically jumps to the React module. The packaging information shows webpack external (using external extensions) as react_DLL.

Without the DllReferencePlugin inside webpack.config.js, the whole packaging process takes about 5S, which is reduced by 2S after use.

Copying DLL files

The DLL file of the third-party library must be inserted into the HTML and copied to the build directory of the project; otherwise, the project cannot run.

Use copy-webpack-plugin to copy the DLL to the build directory, use htMl-webpack-tags-plugin to insert additional

yarn add copy-webpack-plugin html-webpack-tags-plugin -D
Copy the code
module.exports = {
  plugin: [
    new CopyWebpackPlugin({
      patterns: [{from: './dll/react.dffd2b4e9672e773b9c9.dll.js'.to: 'static/js'},]}),new HtmlWebpackPlugin({
      inject: true.template: './public/index.html'.favicon: './public/favicon.ico',}).new HtmlWebpackTagsPlugin({
      append: true.publicPath: 'static/js'.// DLL. Js file path prefix
      tags: ['react.dffd2b4e9672e773b9c9.dll.js'],}),]};Copy the code

Conflict with SplitChunksPlugin

In my tests, if WebPack is configured with SplitChunksPlugin to extract code from node_modules, the DllReferencePlugin does conflict.

Because my test project is very small, I only introduced react and React-DOM, two third-party libraries, and now I have extracted them through DllPlugin and packaged them separately, so there should not be vendor chunk in the project. However, no matter in the development environment or the production environment, If SplitChunksPlugin is configured as follows, the React part will be packaged.

module.exports = {
  optimization: {
    splitChunks: {
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors'.chunks: 'all',},},},},};/ / or
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',}}};Copy the code

externals

Externals can remove modules directly from the packaging process, reducing the workload of webpack packaging and speeding up builds.

Configuration items

Regular expression

You can specify a regular expression for externals, and all modules with matching names will be ignored during packaging

module.exports = {
  externals: /react/};Copy the code

object

Externals can be specified as an object, with the key representing the ignored module name and the name representing the global variables exposed by the Library. For example, using React would import the React and ReactDOM apis

import React from 'react';
import ReactDOM from 'react-dom';
Copy the code

React and “react-dom” indicate that webpack excludes modules from import above. To replace these modules, use global variables like react and ReactDOM.

module.exports = {
  externals: {
    react: 'React'.'react-dom': 'ReactDOM',}};Copy the code

After the yarn Build package is executed, you can see that the Webpack external has global variables for React and ReactDOM.

To be able to find global variables like React and ReactDOM, you need to put the React library in HTML and import it globally via

<script crossorigin src="react.production.min.js"></script>
<script crossorigin src="react-dom.production.min.js"></script>
Copy the code

For structures with a parent module, you can also pass in an array as the property name, where./math is in the parent module, indicating that only the other module inside./math is used, so the result is eventually compiled to require(‘./math’).

module.exports = {
  / /...
  externals: {
    subtract: ['./math'.'other'],}};Copy the code

The DllPlugin may not be needed

The DllPlugin itself is more useful for isolating third-party libraries into separate libraries, and it also needs to expose global variables that are injected globally via

So why not just use CDN +externals? Import resources through the CDN, and then directly externals ignore the module package is done!

CDN can be configured in many ways. Firstly, CDN resources can be directly manually placed on the HTML template page. HtmlWebpackPlugin will automatically generate THE HTML page with CDN resources.

For example, add the React jsDelivr CDN link to the HTML page when externals is configured above and React is ignored.

< script crossorigin SRC = "https://cdn.jsdelivr.net/npm/[email protected]/umd/react.production.min.js" > < / script > < script Crossorigin SRC = "https://cdn.jsdelivr.net/npm/[email protected]/umd/react-dom.production.min.js" > < / script >Copy the code

HtmlWebpackPlugin supports EJS templates by default. For incoming HtmlWebpackPlugin configuration items can be in HTML % > < % = HtmlWebpackPlugin. Options. XXX in the form of a visit.

The React devTools plugin in the browser does not detect that the React application is in the development environment, so it is useless.

module.exports = {
  plugins: [
    new HtmlWebpackPlugin({
      inject: true.template: './public/index.html'.favicon: './public/favicon.ico'.cdn: {
        script: [
          isDevelopment
            ? 'https://cdn.jsdelivr.net/npm/[email protected]/umd/react.development.js'
            : 'https://cdn.jsdelivr.net/npm/[email protected]/umd/react.production.min.js',
          isDevelopment
            ? 'https://cdn.jsdelivr.net/npm/[email protected]/umd/react-dom.development.js'
            : 'https://cdn.jsdelivr.net/npm/[email protected]/umd/react-dom.production.min.js',],},}),],};Copy the code

Modify HTML page, EJS itself is very simple, directly in HTML to write each line of JS code with <%… %> just wrap it, if it’s a variable, use <%=… %> wrap.

<! DOCTYPE html> <html lang="zh-hans"> <head> <meta charset="utf-8" /> <title>toycra</title> </head> <body> <div id="root"></div> <! --CDN--> <% if (htmlWebpackPlugin.options.cdn) { %> <% for(let src of htmlWebpackPlugin.options.cdn.script) { %> <script  crossorigin="anonymous" src="<%=src%>"></script> <% } %> <% } %> </body> </html>Copy the code

Now that the package is executed, the React module is ignored by the externals configuration and uses CDN global injection.