A 10,000-word long article explaining Webpack5 advanced optimization

This article optimizes webpack and code from 4 perspectives:

1. Improve development experience
. Use Source Map to provide more accurate error prompts when code errors are reported during development or online.
2. Improve the speed of packaging and building.
Use HotModuleReplacement to allow only the code that has changed to be recompiled, packaged and updated during development, and the unchanged code is cached, thus making the update faster.
Use OneOf so that once the resource file is processed by a loader, it will not continue to be traversed, and the packaging speed will be faster.
Use Include/Exclude to exclude or only detect certain files, processing fewer files and faster.
Use Cache to cache the results of eslint and babel processing to make the second packaging faster.
Use Thead multi-process to handle eslint and babel tasks faster. (It should be noted that process startup communication has overhead, and it is only effective when using a lot of code processing)
3. Reduce code size.
Use Tree Shaking to eliminate unused redundant code, making the code size smaller.
Use the @babel/plugin-transform-runtime plug-in to process babel so that auxiliary code can be imported from it instead of generating auxiliary code for each file, resulting in a smaller size.
Use Image Minimizer to compress images in the project, resulting in smaller sizes and faster request times. (It should be noted that if the pictures in the project are all online links, then this is not necessary. Only static pictures of local projects need to be compressed.) 4. Optimize the code
running performance
and use Code Split to split the code into multiple js files. This makes the size of a single file smaller and the parallel loading of js faster. And use the import dynamic import syntax to load on demand, so that the resource is loaded only when it is needed, and the resource is not loaded when not in use.
Use Preload / Prefetch to load the code in advance so that it can be used directly when needed in the future, resulting in a better user experience.
Using Network Cache can better name the output resource files so that they can be cached in the future, resulting in a better user experience.
Use Core-js to perform compatibility processing on js so that our code can run in lower version browsers.
Using PWA allows code to be accessed offline, thereby improving user experience.
Insert image description here


Improve development experience

The code that runs during development is compiled by webpack. All css and js are merged into one file, and other codes are added. At this time, if the code runs incorrectly, we will not be able to understand the error location of the code. Once there are a lot of code files developed in the future, it will be difficult to find where the errors occur. Therefore, more accurate error prompts are needed to better develop code.

SourceMap

SourceMap is a scheme for generating files that map source code to built code.
It will generate a xxx.map file, which contains the mapping relationship between each row and column of the source code and the built code. When an error occurs in the built code, the xxx.map file will be used to find the mapped source code error location from the post-build code error location, allowing the browser to prompt the source code file error location and help us find the source of the error faster.

The two most commonly used ones:

Development mode: cheap-module-source-map
Advantages: Fast packaging and compilation, only contains row mapping
Disadvantages: No column mapping (that is, you can only see which rows have errors, but not which columns have errors)

module.exports = {
    
    
  // 其他省略
  mode: "development",
  devtool: "cheap-module-source-map",
};

Production mode: source-map
Advantages: Includes row/column mapping
Disadvantages: Packed compilation is slower

module.exports = {
    
    
  // 其他省略
  mode: "production",
  devtool: "source-map",
};

Improve packaging and build speed

When you modify a module code during development, Webpack will repackage and compile all modules by default, which is very slow. Therefore, if you want to modify a certain module code, only this module code needs to be repackaged and compiled, while other modules remain unchanged, so that the packaging speed can be very fast.

HotModuleReplacement

HotModuleReplacement (HMR hot module replacement): Replace, add or remove modules while the program is running without reloading the entire page.

Basic configuration : add in devServer:

hot: true, // 开启HMR功能(只能用于开发环境,生产环境不需要了)

At this time, the css style has been processed by style-loader and has the HMR function. But js is not good enough.
JS configuration: add in main.js:

// 判断是否支持HMR功能
if (module.hot) {
    
    
  module.hot.accept("./js/count.js", function () {
    
    });// 第二个参数可选,一旦发生热替换可以执行这个函数的功能
  module.hot.accept("./js/sum.js");
}

Writing the above will be very troublesome, so in actual development we will use other loaders to solve it.

For example: vue-loader , react-hot-loader .

OneOf

When packaging, each file will be processed by all loaders. Although it is not actually processed due to test regularity, it must be processed again. slower. Oneof can only match the previous loader, and the rest will not match.

Usage: Use to {oneOf: [ ]}wrap all tests, and the production mode is also configured in the same way.

rules: [
  {
    
    
    oneOf: [
      {
    
    
        // 用来匹配 .css 结尾的文件
        test: /\.css$/,
        // use 数组里面 Loader 执行顺序是从右到左
        use: ["style-loader", "css-loader"],
      },
      ……
    ]
  }
]

Include/Exclude

When developing using third-party libraries or plug-ins, all files are downloaded to node_modules. These files can be used directly without compilation. Therefore, when we process js files, we must exclude the files under node_modules.

include: include, only process xxx files;
exclude: exclude, process all files except xxx files.

Usage: Only one of the two can be used, not at the same time. Production mode is configured similarly.

{
    
    
  test: /\.js$/,
  // exclude: /node_modules/, // 排除node_modules代码不编译
  include: path.resolve(__dirname, "../src"), // 也可以用包含
  loader: "babel-loader",
},

new ESLintWebpackPlugin({
    
    
  // 指定检查文件的根目录
  context: path.resolve(__dirname, "../src"),
  exclude: "node_modules", // 默认值
}),

Cache

Each time the js file is packaged, it must be checked by Eslint and compiled by Babel, which is relatively slow. Therefore, the previous Eslint check and Babel compilation results can be cached, so that the second packaging speed will be faster.

{
    
    
  test: /\.js$/,
  // exclude: /node_modules/, // 排除node_modules代码不编译
  include: path.resolve(__dirname, "../src"), // 也可以用包含
  loader: "babel-loader",
  options: {
    
    
    cacheDirectory: true, // 开启babel编译缓存
    cacheCompression: false, // 缓存文件不要压缩
  },
}
……
new ESLintWebpackPlugin({
    
    
  // 指定检查文件的根目录
  context: path.resolve(__dirname, "../src"),
  exclude: "node_modules", // 默认值
  cache: true, // 开启缓存
  // 缓存目录
  cacheLocation: path.resolve(
    __dirname,
    "../node_modules/.cache/.eslintcache"
  ),
}),

Thread

When the project becomes larger and larger, the packaging speed becomes slower and slower. If you want to continue to improve the packaging speed, you actually need to improve the packaging speed of js, because there are relatively few other files. The main tools for processing js files are eslint, babel, and Terser , so their running speed must be improved.

Thread can open multiple processes to process js files at the same time, which is faster than the previous single-process packaging. Note that it is only used in particularly time-consuming operations, because each process startup has an overhead of about 600ms.

Usage:
1. Download the package: npm i thread-loader -D
2. Configure webpack.prod.js:

// nodejs核心模块,直接使用
const os = require("os");
// cpu核数
const threads = os.cpus().length;
const TerserPlugin = require("terser-webpack-plugin");// 压缩js
……
{
    
    
  test: /\.js$/,
  // exclude: /node_modules/, // 排除node_modules代码不编译
  include: path.resolve(__dirname, "../src"), // 也可以用包含
  use: [
    {
    
    
      loader: "thread-loader", // 开启多进程
      options: {
    
    
        workers: threads, // 数量
      },
    },
    {
    
    
      loader: "babel-loader",
      options: {
    
    
        cacheDirectory: true, // 开启babel编译缓存
      },
    },
  ],
},
……
plugins: [
  new ESLintWebpackPlugin({
    
    
    // 指定检查文件的根目录
    context: path.resolve(__dirname, "../src"),
    exclude: "node_modules", // 默认值
    cache: true, // 开启缓存
    // 缓存目录
    cacheLocation: path.resolve(
      __dirname,
      "../node_modules/.cache/.eslintcache"
    ),
    threads, // 开启多进程
  }),
  // css压缩和放到optimization一样
  // new CssMinimizerPlugin(),
],
optimization: {
    
    
  minimize: true,
  minimizer: [
    // css压缩也可以写到optimization.minimizer里面,效果一样的
    new CssMinimizerPlugin(),
    // 当生产模式会默认开启TerserPlugin,但是我们需要进行其他配置,就要重新写了
    new TerserPlugin({
    
    
      parallel: threads // 开启多进程
    })
  ],
},

Reduce code size

Tree Shaking

Some tool function libraries are defined during development, or third-party tool function libraries or component libraries are referenced. If there is no special processing, the entire library will be introduced when packaging, but in fact only a very small part of the functions may be used. If the entire library is packaged, the size will be too large.

Tree Shaking is often used to describe removing unused code from JavaScript. (It depends on ES Module)

Webpack has enabled this feature by default and no additional configuration is required.

Babel

Babel inserts auxiliary code for each file compiled, making the code size too large, and uses very small auxiliary code for some public methods, such as _extend. By default it will be added to every file that requires it. These auxiliary codes can be used as an independent module to avoid repeated introduction.

@babel/plugin-transform-runtime: Disables Babel's automatic per-file runtime injection. Instead, introduce @babel/plugin-transform-runtime and have all auxiliary code referenced from here.

Usage:
1. Download the package: npm i @babel/plugin-transform-runtime -D
2. Add in babel-loader:

{
    
    
  loader: "babel-loader",
  options: {
    
    
    plugins: ["@babel/plugin-transform-runtime"], // 减少代码体积
  },
},

Image Minimizer

If more pictures are referenced in the development project, the picture size will be larger and the request speed will be slower in the future. Images can be compressed to reduce image size. Online link images are not required, only local project static images need to be compressed.

Usage:
1. Download package: npm i image-minimizer-webpack-plugin imagemin -D
There are remaining packages that need to be downloaded. There are two modes:
lossless compression: npm install imagemin-gifsicle imagemin-jpegtran imagemin-optipng imagemin-svgo -D
lossy compression: npm install imagemin-gifsicle imagemin-mozjpeg imagemin-pngquant imagemin-svgo -D
2. Configuration: Take the lossless compression configuration as an example:

const ImageMinimizerPlugin = require("image-minimizer-webpack-plugin");
……
optimization: {
    
    
  minimizer: [
    // 压缩图片
    new ImageMinimizerPlugin({
    
    
      minimizer: {
    
    
        implementation: ImageMinimizerPlugin.imageminGenerate,
        options: {
    
    
          plugins: [
            ["gifsicle", {
    
     interlaced: true }],
            ["jpegtran", {
    
     progressive: true }],
            ["optipng", {
    
     optimizationLevel: 5 }],
            [
              "svgo",
              {
    
    
                plugins: [
                  "preset-default",
                  "prefixIds",
                  {
    
    
                    name: "sortAttrs",
                    params: {
    
    
                      xmlnsOrder: "alphabetical",
                    },
                  },
                ],
              },
            ],
          ],
        },
      },
    }),
  ],
},

An error may occur during packaging:

Error: Error with ‘src\images\1.jpeg’: ‘“C:\Users\86176\Desktop\webpack\webpack_code\node_modules\jpegtran-bin\vendor\jpegtran.exe”’
Error with ‘src\images\3.gif’: spawn C:\Users\86176\Desktop\webpack\webpack_code\node_modules\optipng-bin\vendor\optipng.exe ENOENT

Two files need to be installed into node_modules to solve the problem:
jpegtran.exe needs to be copied to node_modules\jpegtran-bin\vendor.
optipng.exe needs to be copied to node_modules\optipng-bin\vendor.

Optimize code running performance

Code Split

When packaging code, all js files will be packaged into one file, which is too large. If you only want to render the home page, only the js file of the home page should be loaded, and other files should not be loaded. Therefore, it is necessary to code-split the files generated by packaging, generate multiple js files, and load which js file is used to render the page. In this way, fewer resources will be loaded and the speed will be faster.

There are different ways to implement code splitting.

1. Multiple entrances

1. Download the package: npm i webpack webpack-cli html-webpack-plugin -D
2. Configure webpack.config.js:

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
    
    
  // 单入口
  // entry: './src/main.js',
  // 多入口
  entry: {
    
    
    main: "./src/main.js",
    app: "./src/app.js",
  },
  output: {
    
    
    path: path.resolve(__dirname, "./dist"),
    // [name]是webpack命名规则,使用chunk的name作为输出的文件名。
    // 什么是chunk?打包的资源就是chunk,输出出去叫bundle。
    // chunk的name是啥呢? 比如: entry中xxx: "./src/xxx.js", name就是xxx。注意是前面的xxx,和文件名无关。
    // 为什么需要这样命名呢?如果还是之前写法main.js,那么打包生成两个js文件都会叫做main.js会发生覆盖。(实际上会直接报错的)
    filename: "js/[name].js",
    clear: true,
  },
  plugins: [
    new HtmlWebpackPlugin({
    
    
      template: "./public/index.html",
    }),
  ],
  mode: "production",
};

3. Run the command npx webpack
and configure several entrances to output at least several js files.

2. Extract duplicate codes

If the same code is referenced in multiple entry files, we do not want this code to be packaged into two files, resulting in code duplication and larger size. At this time, you need to extract the repeated code of multiple entries, only package it to generate a js file, and other files can reference it. Add: to
webpack.config.js :module.exports = { }

optimization: {
    
    
  // 代码分割配置
  splitChunks: {
    
    
    chunks: "all", // 对所有模块都进行分割
    // 以下是默认值
    // minSize: 20000, // 分割代码最小的大小
    // minRemainingSize: 0, // 类似于minSize,最后确保提取的文件大小不能为0
    // minChunks: 1, // 至少被引用的次数,满足条件才会代码分割
    // maxAsyncRequests: 30, // 按需加载时并行加载的文件的最大数量
    // maxInitialRequests: 30, // 入口js文件最大并行请求数量
    // enforceSizeThreshold: 50000, // 超过50kb一定会单独打包(此时会忽略minRemainingSize、maxAsyncRequests、maxInitialRequests)
    // cacheGroups: { // 组,哪些模块要打包到一个组
    //   defaultVendors: { // 组名
    //     test: /[\\/]node_modules[\\/]/, // 需要打包到一起的模块
    //     priority: -10, // 权重(越大越高)
    //     reuseExistingChunk: true, // 如果当前 chunk 包含已从主 bundle 中拆分出的模块,则它将被重用,而不是生成新的模块
    //   },
    //   default: { // 其他没有写的配置会使用上面的默认值
    //     minChunks: 2, // 这里的minChunks权重更大
    //     priority: -20,
    //     reuseExistingChunk: true,
    //   },
    // },
    // 修改配置
    cacheGroups: {
    
    
      // 组,哪些模块要打包到一个组
      // defaultVendors: { // 组名
      //   test: /[\\/]node_modules[\\/]/, // 需要打包到一起的模块
      //   priority: -10, // 权重(越大越高)
      //   reuseExistingChunk: true, // 如果当前 chunk 包含已从主 bundle 中拆分出的模块,则它将被重用,而不是生成新的模块
      // },
      default: {
    
    
        // 其他没有写的配置会使用上面的默认值
        minSize: 0, // 我们定义的文件体积太小了,所以要改打包的最小文件体积
        minChunks: 2,
        priority: -20,
        reuseExistingChunk: true,
      },
    },
  },

run commandnpx webpack

3. Load on demand, dynamic import

To achieve on-demand loading, dynamically import modules. Additional configuration is required:

1. Modify the file main.js:

console.log("hello main");
document.getElementById("btn").onclick = function () {
    
    
  // 动态导入 --> 实现按需加载
  // 即使只被引用了一次,也会代码分割
  import("./math.js").then(({
     
      sum }) => {
    
    
    alert(sum(1, 2, 3, 4, 5));
  });
};

app.js:

console.log("hello app");

2. Run the commandnpx webpack

4. Single entrance

When developing, it may be a single page application (SPA) with only one entrance (single entry). It needs to be configured like this: Add
to webpack.config.js module.exports = { }:

optimization: {
    
    
  // 代码分割配置
  splitChunks: {
    
    
    chunks: "all", // 对所有模块都进行分割
  }
}

Preload / Prefetch

Code splitting has been done previously, and the import dynamic import syntax will be used to load code on demand. But the loading speed is not good enough. For example, the resource is loaded only when the user clicks the button. If the resource is large, the user will feel an obvious lag effect.

Optimization: During the browser's idle time, resources that need to be used later are loaded. You need Preload (to tell the browser to load resources immediately) or Prefetch (to tell the browser to start loading resources when it is idle).

What they have in common: they all only load resources and do not execute them; they all have caches.

Difference: Preload has a high loading priority and Prefetch has a low loading priority; Preload can only load the resources needed for the current page, while Prefetch can load the resources needed for the current page and the resources needed for the next page.

Summary: Use Preload to load resources with high priority on the current page; use Prefetch to load resources that need to be used on the next page.

Their problem: poor compatibility. Preload has better compatibility than Prefetch.

Usage:
1. Download the package: npm i @vue/preload-webpack-plugin -D
2. Configure webpack.prod.js:

const PreloadWebpackPlugin = require("@vue/preload-webpack-plugin");
……
plugins:[
  new PreloadWebpackPlugin({
    
    
    rel: "preload", // preload兼容性更好
    as: "script",
    // rel: 'prefetch' // prefetch兼容性更差
  }),
],

Network Cache

Static resources will be optimized using cache, so that the browser can read the cache the second time it requests the resource, which is very fast. But there will be a problem in this case, because the file names output before and after are the same, both called main.js. Once a new version is released in the future, because the file name has not changed, the browser will directly read the cache and will not load new resources. , the project cannot be updated. So start with the file name and make sure the file name is different before and after the update, so that you can cache it.

The following will generate a unique hash value:

fullhash (webpack4 is hash): Every time any file is modified, the hash of all file names will change. So once any file is modified, the file cache of the entire project will be invalid.

Chunkhash : parses dependent files based on different entry files (Entry), builds corresponding chunks, and generates corresponding hash values. Our js and css are imported from the same import and will share a hash value.

contenthash : Generate a hash value based on the file content. The hash value will change only if the file content changes. All file hash values ​​are unique and different.

usage:

output: {
    
    
  filename: "static/js/[name].[contenthash:8].js", // 入口文件打包输出资源命名方式,[contenthash:8]使用contenthash,取8位长度
  chunkFilename: "static/js/[name].[contenthash:8].chunk.js", // 动态导入输出资源命名方式
},
……
plugins: [
  // 提取css成单独文件
  new MiniCssExtractPlugin({
    
    
    // 定义输出文件名和目录
    filename: "static/css/[name].[contenthash:8].css",
    chunkFilename: "static/css/[name].[contenthash:8].chunk.css",
  }),
]

When the math.js file is modified and repackaged, the hash value of the math.js file changes due to contenthash (this is normal). But this will invalidate the cache of main.js. When only math.js is modified, why does main.js also change?

Reason:
Before the update: math.xxx.js referenced by math.xxx.js,
after the update: math.yyy.js, main.js referenced by math.yyy.js, the file name has changed, which indirectly caused main .js has also changed

Solution: Store the hash value separately in a runtime file, and finally output three files: main, math, and runtime. When the math file sends changes, the changes are to the math and runtime files, and main remains unchanged. The runtime file only saves the hash values ​​of the files and their relationship with the files. The entire file size is relatively small, so the cost of changing and re-requesting is also small.

optimization: {
    
    
  // 提取runtime文件
  runtimeChunk: {
    
    
    name: (entrypoint) => `runtime~${
      
      entrypoint.name}`, // runtime文件命名规则
  },
}

Core-js

Previously, babel was used to handle compatibility of js code, and @babel/preset-envsmart presets were used to handle compatibility issues. It can compile and convert some ES6 syntax, such as arrow functions, dot-dot operators, etc. But if it is an async function, a promise object, some methods of an array (includes), etc., it cannot handle it. Therefore, the js code still has compatibility issues at this time, and an error will be reported directly if it encounters a lower version of the browser. The js compatibility issue needs to be completely resolved.

core-jsIt is a polyfill (shim/patch) specially used for ES6 and above APIs. It uses a piece of code provided by the community to use the new features on browsers that are not compatible with certain new features.

Usage:
1. Modify main.js:

// 添加promise代码
const promise = Promise.resolve();
promise.then(() => {
    
    
  console.log("hello promise");
});

2. Download the package: npm i core-js
3. Import main.js:
1) Manually introduce all

import "core-js";

This will introduce all the compatibility code, which is too large. We only want to introduce the promise polyfill.
2) Manually introduce it on demand and find the required package in node_moudle.

import "core-js/es/promise";

Only the polyfill for packaging promises is introduced, and the packaging volume is smaller. But if I want to use other syntaxes in the future, it will be troublesome for me to manually introduce the library.

3) Automatically import
main.js on demand without importing; modify babel.config.js:

module.exports = {
    
    
  // 智能预设:能够编译ES6语法
  presets: [
    [
      "@babel/preset-env",
      // 按需加载core-js的polyfill
      {
    
     useBuiltIns: "usage", corejs: {
    
     version: "3", proposals: true } },
    ],
  ],
};

At this point, the corresponding polyfill will be automatically loaded on demand based on the syntax used in our code.

PWA

When developing a Web App project, once the project is offline, it will be inaccessible.

Progressive web application (PWA): It is a technology that can provide a Web App similar to a native app (native application) experience, and the application can continue to run when offline. Internally implemented through Service Workers technology.

Usage:
1. Download package:npm i workbox-webpack-plugin -D

2. Modify webpack.prod.js:

const WorkboxPlugin = require("workbox-webpack-plugin");
……
plugins: [
	new WorkboxPlugin.GenerateSW({
    
    
      // 这些选项帮助快速启用 ServiceWorkers
      // 不允许遗留任何“旧的” ServiceWorkers
      clientsClaim: true,
      skipWaiting: true,
    }),
]

3.Add in main.js:

if ("serviceWorker" in navigator) {
    
    
  window.addEventListener("load", () => {
    
    
    navigator.serviceWorker
      .register("/service-worker.js")
      .then((registration) => {
    
    
        console.log("SW registered: ", registration);
      })
      .catch((registrationError) => {
    
    
        console.log("SW registration failed: ", registrationError);
      });
  });
}

4. Run the command: npm run build
If you access the packaged page directly through VSCode at this time, you will find SW registration failed in the browser console.

Because the access path is: http://127.0.0.1:5500/dist/index.html. At this time, the page will request the service-worker.js file. The request path is: http://127.0.0.1:5500/service-worker.js. If it is not found, a 404 will appear. The actual service-worker.js file path is: http://127.0.0.1:5500/dist/service-worker.js.

5. Solve the path problem.
Download package: npm i serve -g
serve is also used to start the development server to deploy the code and view the effect.
Run command: serve dist
dist is the deployment directory. At this time, the server service-worker started through serve can be registered successfully.

Reference documentation

Guess you like

Origin blog.csdn.net/zag666/article/details/131932150