Webpack优化篇

前置

性能分析工具

这里使用 Webpack-bundle-analyzer

1
pnpm add progress-bar-webpack-plugin -D

配置

1
2
3
4
5
6
7
8
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
// ...
plugins: [
// 代码产物分析
new BundleAnalyzerPlugin(),
],
}

分析界面(图片来自https://www.npmjs.com/package/webpack-bundle-analyzer)

webpack 构建流程

  • 初始化
    1. 根据配置文件、Shell 参数以及默认配置结合得出最终的配置参数
    2. 创建编译器对象 compiler
    3. 初始化编译环境
    4. 编译开始,执行 compiler 的 run 方法,创建 compilation 对象
    5. 确定入口
  • 构建
    1. 编译模块
    2. 完成编译模块,得到依赖关系图
  • 封装
    1. 根据依赖关系进行打包
    2. 对包进行优化,tree-shaking、压缩 等
    3. 写入系统

webpack 生命周期

  1. beforeRun:进入编译前的阶段,初始化 Compiler 对象。
  2. run:开始编译前的阶段,此时会读取入口文件和依赖,并创建依赖图。
  3. compilation:进入编译阶段,此时会开始编译入口文件和依赖的模块,并生成输出文件。
  4. emit:生成输出文件前的阶段,此时可以在插件中处理生成的输出文件。
  5. done:完成打包后的阶段,此时可以在插件中进行一些清理工作。

缓存

在 webpack 5 中提供了缓存的功能,通过 cache.type = 'filesystem' 即可开启持久化缓存。
开启持久化缓存后,webpack 会在首次构建时,将构建出的产物模块序列化然后保存到硬盘,后面再执行构建时,就可以跳过很多编译过程,直接复用缓存。

图像优化

压缩

关于图片压缩这里使用的是 image-webpack-loader

1
pnpm add -D image-webpack-loader

配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
module.exports = {
// ...
module: {
rules: [
{
test: /\.(gif|png|jpe?g|svg)$/i,
type: "asset/resource",
use: [{
loader: 'image-webpack-loader',
options: {
mozjpeg: {
progressive: true,
quality: 65,
},
optipng: {
enabled: false,
},
pngquant: {
quality: [0.65, 0.9],
speed: 4,
},
gifsicle: {
interlaced: false,
},
webp: {
quality: 75,
},
}
}]
},
]
}
}

实际测试 766kb 图片压缩到192kb。

  • mozjpeg:压缩 JPG(JPEG) 图片
  • optipng、pngquant:压缩 PNG 图片
  • svgo:压缩 SVG 图片
  • gifsicle:压缩 Gif 图
  • webp:将 JPG/PNG 图压缩并转化为 WebP 图片格式

打包体积优化

使用 cdn 分包

将项目中体积较大的包通过 cdn 的方式引入,减少打包产物体积大小。

1
pnpm add html-webpack-externals-plugin -D

配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const HtmlWebpackExternalsPlugin = require("html-webpack-externals-plugin");
module.exports = merge(common, {
// ...
plugins: [
// ...
new HtmlWebpackExternalsPlugin({
externals: [
{
module: 'react',
entry: 'https://unpkg.com/react@18.2.0/umd/react.production.min.js',
global: 'React',
},
{
module: 'react-dom',
entry:
'https://unpkg.com/react-dom@18.2.0/umd/react-dom.production.min.js',
global: 'ReactDOM',
},
{
module: 'echarts',
entry:
'https://cdn.bootcdn.net/ajax/libs/echarts/5.4.3/echarts.common.min.js',
global: 'echarts',
},
]
})
],
});

这里以 react、react-dom、echarts 为例,将他们使用 cdn 的方式引入,不再打包进入产物,

合并模块 Scope Hoisting

Scope Hoisting 可以将符合条件的多个模块合并到一个函数中,减少产物体积

开启方式

  • 使用 mode = ‘production’ 开启生产模式
  • 使用 optimization.concatenateModules 配置项
  • 使用 ModuleConcatenationPlugin 插件

特殊情况

在一些特殊情况下会关闭模块合并

  1. 非 ESM 模块
  2. 模块被多个 Chunk 引用

打包速度优化

多进程打包

1
pnpm add -D thread-loader

配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
module.exports = {
// ...
module: {
rules: [
// ...
{
test: /\.js$/,
use: [
{
loader: "thread-loader",
options: {
workers: 2,
workerParallelJobs: 50,
},
},
// ...
]
}
]
}
}
  • workers 进程总数
  • workerParallelJobs 单个进程中并发执行的任务数
  • poolTimeout 超时时间,子进程空闲超时会关闭
  • poolRespawn 是否允许子进程关闭后重新创建新的子进程
  • workerNodeArgs 设置启动子进程时,额外的参数

按需编译

webpack 提供一个实验特性 lazyCompilation用来实现异步引用模块的按需编译,极大提高了冷启动速度。

1
2
3
4
5
6
module.exports = {
// ...
experiments: {
lazyCompilation: true,
},
};

但是该功能还处在实验阶段,最好只在开发环境使用

约束 loader 执行的范围

通过 module.rules.exclude / module.rules.include 可以进一步缩小 loader 的执行范围。

1
2
3
4
5
6
7
8
9
10
11
12
module.exports = {
// ...
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
// ...
},
],
},
};

使用 noParse

使用 module.noParse 跳过不需要二次编译的资源文件。

1
2
3
4
5
6
module.exports = {
//...
module: {
noParse: /lodash|react/,
},
};

使用 noParse 需要注意的是:

  • 需要确保 noParse 文件的准确性
  • 不能依赖其他文件
  • 使用 noParse 的文件就无法 tree-shaking 了

关闭 ts 的类型检查

使用 fork-ts-checker-webpack-plugin 插件来进行 ts 的类型检查,该插件会将 ts 的类型检查和编译过程放在单独的进程中运行,然后我们关闭 ts-loader 的类型检查,提升编译速度。

1
pnpm add -D fork-ts-checker-webpack-plugin

配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');

module.exports = {
// ...
module: {
rules: [{
test: /\.ts$/,
use: [
{
loader: 'ts-loader',
options: {
// 仅编译
transpileOnly: true
}
}
],
}, ],
},
plugins:[
// 使用插件,用单独的进程进行 ts 类型检查
new ForkTsCheckerWebpackPlugin()
]
};

resolve.modules 定向查找

webpack 在查找模块时,会先去当前目录的 ./node_modules 下查找,没找到就去上一级的 ../node_modules 找。使用 resolve.modules 指定查找的目录,减少查找的时间

1
2
3
4
5
module.export = {
resolve: {
modules: [path.resolve(__dirname, 'node_modules')]
}
}

指定补全后缀

resolve.extensions 可以指定导入没写后缀时自动补全的后缀列表,默认值是 ['.js', '.json', '.wasm'],也就是导入的时候没写后缀,会自动去查找 .js、.json、.wasm 后缀的文件,如果找不到就报错。
如果手动声明这个属性,会把默认值覆盖掉,我们可以通过 ['...']的方式保留默认值

1
2
3
4
5
module.export = {
resolve: {
extensions: ["...", '.jsx', '.ts', '.tsx'],
}
}

代码压缩

代码压缩就是抛弃代码的可读性、格式化等等一切,把我们的代码做到最精简。

js 压缩

webpack 5 默认使用 terser 进行 js 压缩,通过 optimization.minimize 属性开启。

css 压缩

使用 css-minimizer-webpack-plugin 插件来完成对 css 的压缩

1
pnpm add -D css-minimizer-webpack-plugin

配置

1
2
3
4
5
6
7
8
9
10
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
module.exports = merge(common, {
// ...
optimization: {
minimize: true,
minimizer: [
new CssMinimizerPlugin(),
],
},
});

该插件还可以通过 minify 属性来指定使用什么进行压缩,可选值:

  • CssMinimizerPlugin.cssnanoMinify 使用 cssnano 默认值,不需要额外安装依赖
  • CssMinimizerPlugin.cssoMinify 使用 csso
  • CssMinimizerPlugin.cleanCssMinify 使用 clean-css
  • CssMinimizerPlugin.esbuildMinify 使用 ESBuild
  • CssMinimizerPlugin.parcelCssMinify 使用 parcel-css

需要注意的是,该插件必须配合之前提到的 mini-css-extract-plugin 插件一起使用,mini-css-extract-plugin 插件将 css 代码抽离到一个文件,css-minimizer-webpack-plugin 再对代码进行压缩。

html 压缩

1
pnpm add -D html-minifier-terser

配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module.exports = merge(common, {
// ...
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
parallel: 2 // number | boolean
}),
new CssMinimizerPlugin({
minify: CssMinimizerPlugin.cssnanoMinify,
}),
],
},
});

gzip

安装 CompressionWebpackPlugin

1
pnpm add -D compression-webpack-plugin

配置

1
2
3
4
5
6
7
8
9
10
11
const CompressionPlugin = require('compression-webpack-plugin');

module.exports = {
// ...
plugins: [
new CompressionPlugin({
algorithm: 'gzip', // 默认值就是 gzip
test: /\.(js|css)$/, // 压缩范围
}),
],
};

抽离运行时代码

在 entry 中配置 runtime 将运行时代码抽离到 common-runtime 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const path = require("path");

module.exports = {
mode: "development",
devtool: false,
entry: {
main: { import: "./src/index.js", runtime: "common-runtime" },
foo: { import: "./src/foo.js", runtime: "common-runtime" },
},
output: {
clean: true,
filename: "[name].js",
path: path.resolve(__dirname, "dist"),
},
};

Webpack优化篇
https://l1ushun.github.io/2024/01/06/webpack-03/
作者
liu shun
发布于
2024年1月6日