前置
性能分析工具
这里使用 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(), ], }
|

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
|
配置
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 插件
特殊情况
在一些特殊情况下会关闭模块合并
- 非 ESM 模块
- 模块被多个 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:[ 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 }), 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', 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"), }, };
|