前置
性能分析工具
这里使用 Webpack-bundle-analyzer
| 1
 | pnpm add progress-bar-webpack-plugin -D
 | 
配置
| 12
 3
 4
 5
 6
 7
 8
 
 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;module.exports = {
 
 plugins: [
 
 new BundleAnalyzerPlugin(),
 ],
 }
 
 | 

webpack 构建流程
- 初始化
- 根据配置文件、Shell 参数以及默认配置结合得出最终的配置参数
- 创建编译器对象 compiler
- 初始化编译环境
- 编译开始,执行 compiler 的 run 方法,创建 compilation 对象
- 确定入口
 
- 构建
- 编译模块
- 完成编译模块,得到依赖关系图
 
- 封装
- 根据依赖关系进行打包
- 对包进行优化,tree-shaking、压缩 等
- 写入系统
 
webpack 生命周期
- beforeRun:进入编译前的阶段,初始化 Compiler 对象。
- run:开始编译前的阶段,此时会读取入口文件和依赖,并创建依赖图。
- compilation:进入编译阶段,此时会开始编译入口文件和依赖的模块,并生成输出文件。
- emit:生成输出文件前的阶段,此时可以在插件中处理生成的输出文件。
- done:完成打包后的阶段,此时可以在插件中进行一些清理工作。
缓存
在 webpack 5 中提供了缓存的功能,通过 cache.type = 'filesystem' 即可开启持久化缓存。
开启持久化缓存后,webpack 会在首次构建时,将构建出的产物模块序列化然后保存到硬盘,后面再执行构建时,就可以跳过很多编译过程,直接复用缓存。
图像优化
压缩
关于图片压缩这里使用的是 image-webpack-loader。
| 1
 | pnpm add -D image-webpack-loader
 | 
配置
| 12
 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
 | 
配置
| 12
 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 插件
特殊情况
在一些特殊情况下会关闭模块合并
- 非 ESM 模块
- 模块被多个 Chunk 引用
打包速度优化
多进程打包
| 1
 | pnpm add -D thread-loader
 | 
配置
| 12
 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用来实现异步引用模块的按需编译,极大提高了冷启动速度。
| 12
 3
 4
 5
 6
 
 | module.exports = {
 experiments: {
 lazyCompilation: true,
 },
 };
 
 | 
但是该功能还处在实验阶段,最好只在开发环境使用。
约束 loader 执行的范围
通过 module.rules.exclude / module.rules.include 可以进一步缩小 loader 的执行范围。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 
 | module.exports = {
 module: {
 rules: [
 {
 test: /\.js$/,
 exclude: /node_modules/,
 
 },
 ],
 },
 };
 
 | 
使用 noParse
使用 module.noParse 跳过不需要二次编译的资源文件。
| 12
 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
 | 
配置
| 12
 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:[
 
 new ForkTsCheckerWebpackPlugin()
 ]
 };
 
 | 
resolve.modules 定向查找
webpack 在查找模块时,会先去当前目录的 ./node_modules 下查找,没找到就去上一级的 ../node_modules 找。使用 resolve.modules 指定查找的目录,减少查找的时间
| 12
 3
 4
 5
 
 | module.export = {resolve: {
 modules: [path.resolve(__dirname, 'node_modules')]
 }
 }
 
 | 
指定补全后缀
resolve.extensions 可以指定导入没写后缀时自动补全的后缀列表,默认值是 ['.js', '.json', '.wasm'],也就是导入的时候没写后缀,会自动去查找 .js、.json、.wasm 后缀的文件,如果找不到就报错。
如果手动声明这个属性,会把默认值覆盖掉,我们可以通过 ['...']的方式保留默认值
| 12
 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
 | 
配置
| 12
 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
 | 
配置
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 
 | module.exports = merge(common, {
 optimization: {
 minimize: true,
 minimizer: [
 new TerserPlugin({
 parallel: 2
 }),
 new CssMinimizerPlugin({
 minify: CssMinimizerPlugin.cssnanoMinify,
 }),
 ],
 },
 });
 
 | 
gzip
安装 CompressionWebpackPlugin
| 1
 | pnpm add -D compression-webpack-plugin
 | 
配置
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 
 | const CompressionPlugin = require('compression-webpack-plugin');
 module.exports = {
 
 plugins: [
 new CompressionPlugin({
 algorithm: 'gzip',
 test: /\.(js|css)$/,
 }),
 ],
 };
 
 | 
抽离运行时代码
在 entry 中配置 runtime 将运行时代码抽离到 common-runtime 中
| 12
 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"),
 },
 };
 
 |