1、实现目标
本篇将从优化开发体验、加快编译速度、减小打包体积、加快加载速度 4 个角度出发,介绍如何对 webpack 项目进行优化;
2、效率工具
2.1 编译进度条
https://www.npmjs.com/package/progress-bar-webpack-plugin
安装:
npm i -D progress-bar-webpack-plugin
webpack.common.js 配置方式如下:
const chalk = require('chalk')const ProgressBarPlugin = require('progress-bar-webpack-plugin')module.exports = {plugins: [// 进度条new ProgressBarPlugin({format: ` :msg [:bar] ${chalk.green.bold(':percent')} (:elapsed s)`})],}

2.2 编译速度分析
https://www.npmjs.com/package/speed-measure-webpack-plugin
安装:
npm i -D speed-measure-webpack-plugin
webpack.dev.js 配置方式如下:
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");const smp = new SpeedMeasurePlugin();module.exports = smp.wrap({// ...webpack config...})
效果:
2.3 打包体积分析
https://www.npmjs.com/package/webpack-bundle-analyzer
安装:
npm i -D webpack-bundle-analyzer
webpack.prod.js 配置方式如下:
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;module.exports = {plugins: [// 打包体积分析new BundleAnalyzerPlugin()],}
包含各个 bundle 的体积分析,效果如下:
3、优化开发
3.1 热更新
热更新指的是,在开发过程中,修改代码后,仅更新修改部分的内容,无需刷新整个页面;
webpack.dev.js 配置方式如下:
module.export = {devServer: {contentBase: './dist',hot: true, // 热更新},}
3.1 热更新React组件
使用 react-refresh-webpack-plugin热更新 react 组件;
安装:
npm install -D @pmmmwh/react-refresh-webpack-plugin react-refresh
webpack.dev.js 配置方式如下:
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');module.exports = {plugins: [new webpack.HotModuleReplacementPlugin(),new ReactRefreshWebpackPlugin(),]}
4、构建速度优化
4.1 cache
webpack5 较于 webpack4,新增了持久化缓存、改进缓存算法等优化,webpack5 新特性可查看 参考资料;
通过配置 webpack 持久化缓存,cache: filesystem,来缓存生成的 webpack 模块和 chunk,改善构建速度,可提速 90% 左右;
webpack.common.js 配置方式如下:
module.exports = {cache: {type: 'filesystem', // 使用文件缓存},}
引入缓存后,首次构建时间将增加 15%,二次构建时间将减少 90%,效果如下:
使用前:                                    使用后第一次构建略微慢点:           使用后第二次构建直接起飞:
        
          
4.2 减少 loader、plugins
每个的 loader、plugin 都有其启动时间,尽量少地使用工具,将非必须的 loader、plugins 删除;
4.3 指定 include
为 loader 指定 include,减少 loader 应用范围,仅应用于最少数量的必要模块。
module.exports = {rules: [{test: /\.(png|svg|jpg|jpeg|gif)$/i,include: [paths.resolveApp('src'),],type: 'asset/resource',}]}
4.4 管理资源
使用 webpack 资源模块(asset module) 代替旧的 assets loader(如 file-loader/url-loader/raw-loader 等),减少 loader 配置数量。
module.exports = {rules: [{test: /\.(png|svg|jpg|jpeg|gif)$/i,include: [paths.resolveApp('src'),],type: 'asset/resource',}]}
4.5 优化 resolve 配置
resolve用来配置 webpack 如何解析模块,可通过优化 resolve 配置来覆盖默认配置项,减少解析范围;
4.5.1 alias
alias 可以创建 import 或 require 的别名,用来简化模块引入;
webpack.common.js 配置方式如下:
module.exports = {resolve: {alias: {'@': paths.appSrc, // @ 代表 src 路径},}}
4.5.2 extensions
extensions 表示需要解析的文件类型列表。
根据项目中的文件类型,定义 extensions,以覆盖 webpack 默认的 extensions,加快解析速度;
由于 webpack 的解析顺序是从左到右,因此要将使用频率高的文件类型放在左侧,如下我将 tsx 放在最左侧;
webpack.common.js 配置方式如下:
module.exports = {resolve: {extensions: ['.tsx', '.ts', '.js'],}}
4.5.3 modules
modules 表示 webpack 解析模块时需要解析的目录;
指定目录可缩小 webpack 解析范围,加快构建速度;
webpack.common.js 配置方式如下:
module.exports = {resolve{modules: ['node_modules',paths.appSrc,]}}
4.5.4 symlinks
如果项目不使用 symlinks(例如 npm link 或者 yarn link),可以设置 resolve.symlinks: false,减少解析工作量。
webpack.common.js 配置方式如下:
module.exports = {resolve: {symlinks: false,},}
4.6 多线程(thread-loader)
通过 thread-loader将耗时的 loader 放在一个独立的 worker 池中运行,加快 loader 构建速度;
安装:
npm i -D thread-loader
配置:
{loader: 'thread-loader',options: {workerParallelJobs: 2}},
4.7 区分环境
切忌在开发环境使用生产环境才会用到的工具,如在开发环境下,应该排除 [fullhash]/[chunkhash]/[contenthash] 等工具。
在生产环境,应该避免使用开发环境才会用到的工具,如 webpack-dev-server 等插件;
4.8 devtool
不同的 devtool 设置,会导致性能差异。在多数情况下,最佳选择是 eval-cheap-module-source-map;
webpack.dev.js配置如下:
export.module = {devtool: 'eval-cheap-module-source-map',}
4.9 输出结果不携带路径信息
默认 webpack 会在输出的 bundle 中生成路径信息,将路径信息删除可小幅提升构建速度。
module.exports = {output: {pathinfo: false,},};}
4.10 IgnorePlugin
IgnorePlugin 在构建模块时直接剔除那些需要被排除的模块,常用于moment和国际化;
new webpack.IgnorePlugin(/\.\/locale/, /moment/)
4.11 DllPlugin
核心思想是将项目依赖的框架等模块单独构建打包,与普通构建流程区分开。
output: {filename: '[name].dll.js',// 输出的文件都放到 dist 目录下path: distPath,library: '_dll_[name]',},plugins: [// 接入 DllPluginnew DllPlugin({// 动态链接库的全局变量名称,需要和 output.library 中保持一致// 该字段的值也就是输出的 manifest.json 文件 中 name 字段的值// 例如 react.manifest.json 中就有 "name": "_dll_react"name: '_dll_[name]',// 描述动态链接库的 manifest.json 文件输出时的文件名称path: path.join(distPath, '[name].manifest.json'),}),],
4.12 Externals
Webpack 配置中的 externals 和 DllPlugin 解决的是同一类问题:将依赖的框架等模块从构建过程中移除。
它们的区别在于:
- 在 Webpack 的配置方面,externals 更简单,而 DllPlugin 需要独立的配置文件。
 - DllPlugin 包含了依赖包的独立构建流程,而 externals 配置中不包含依赖框架的生成方式,通常使用已传入 CDN 的依赖包。
 - externals 配置的依赖包需要单独指定依赖模块的加载方式:全局对象、CommonJS、AMD 等。
 - 在引用依赖包的子模块时,DllPlugin 无须更改,而 externals 则会将子模块打入项目包中。
 
// 引入cdn<scriptsrc="https://code.jquery.com/jquery-3.1.0.js"integrity="sha256-slogkvB1K3VOkzAI8QITxV3VzpOnkeNVsKvtkYLMjfk="crossorigin="anonymous"></script>// webpack配置module.exports = {//...externals: {jquery: 'jQuery',},};// 页面import $ from 'jquery';$('.my-element').animate(/* ... */);
5、减小打包体积
5.1 代码压缩
5.1.1 JS压缩
使用 TerserWebpackPlugin来压缩 JavaScript;
webpack5 自带最新的 terser-webpack-plugin,无需手动安装;
terser-webpack-plugin 默认开启了 parallel: true 配置,并发运行的默认数量: os.cpus().length - 1 ,本文配置的 parallel 数量为 4,使用多进程并发运行压缩以提高构建速度;
webpack.prod.js 配置方式如下:
const TerserPlugin = require('terser-webpack-plugin');module.exports = {optimization: {minimizer: [new TerserPlugin({parallel: 4,terserOptions: {parse: {ecma: 8,},compress: {ecma: 5,warnings: false,comparisons: false,inline: 2,},mangle: {safari10: true,},output: {ecma: 5,comments: false,ascii_only: true,},},}),]}}
5.1.2 CSS压缩
使用 CssMinimizerWebpackPlugin压缩 CSS 文件;
安装:
npm install -D css-minimizer-webpack-plugin
webpack.prod.js 配置方式如下:
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");module.exports = {optimization: {minimizer: [new CssMinimizerPlugin({parallel: 4,}),],}}
5.2 代码分离
代码分离能够把代码分离到不同的 bundle 中,然后可以按需加载或并行加载这些文件。代码分离可以用于获取更小的 bundle,以及控制资源加载优先级,可以缩短页面加载时间;
5.2.1 抽离重复代码
SplitChunksPlugin插件开箱即用,可以将公共的依赖模块提取到已有的入口 chunk 中,或者提取到一个新生成的 chunk;
webpack 将根据以下条件自动拆分 chunks:
- 新的 chunk 可以被共享,或者模块来自于 node_modules 文件夹;
 - 新的 chunk 体积大于 20kb(在进行 min+gz 之前的体积);
 - 当按需加载 chunks 时,并行请求的最大数量小于或等于 30;
 - 当加载初始化页面时,并发请求的最大数量小于或等于 30; 通过 splitChunks 把 react 等公共库抽离出来,不重复引入占用体积;
 
注意:切记不要为 cacheGroups 定义固定的 name,因为 cacheGroups.name 指定字符串或始终返回相同字符串的函数时,会将所有常见模块和 vendor 合并为一个 chunk。这会导致更大的初始下载量并减慢页面加载速度;
webpack.prod.js 配置方式如下:
module.exports = {optimization: {splitChunks: {// include all types of chunkschunks: 'all',// 重复打包问题cacheGroups:{// node_modules里的代码// 第三方模块vendors:{test: /[\\/]node_modules[\\/]/,chunks: "all",// name: 'vendors', 一定不要定义固定的namepriority: 10, // 优先级enforce: true},// 公共的模块common: {name: 'common', // chunk 名称priority: 0, // 优先级minSize: 0, // 公共模块的大小限制minChunks: 2 // 公共模块最少复用过几次}}}}}
5.2.2 CSS文件分离
MiniCssExtractPlugin插件将 CSS 提取到单独的文件中,为每个包含 CSS 的 JS 文件创建一个 CSS 文件,并且支持 CSS 和 SourceMaps 的按需加载;
安装:
npm install -D mini-css-extract-plugin
webpack.common.js 配置方式如下:
注意:MiniCssExtractPlugin.loader 要放在 style-loader 后面;
const MiniCssExtractPlugin = require("mini-css-extract-plugin");module.exports = {plugins: [new MiniCssExtractPlugin()],module: {rules: [{test: /\.module\.(scss|sass)$/,include: paths.appSrc,use: ['style-loader',isEnvProduction && MiniCssExtractPlugin.loader, // 仅生产环境{loader: 'css-loader',options: {modules: true,importLoaders: 2,},},{loader: 'postcss-loader',options: {postcssOptions: {plugins: [['postcss-preset-env',],],},},},{loader: 'thread-loader',options: {workerParallelJobs: 2}},'sass-loader',].filter(Boolean),},]},};
效果:
5.2.3 最小化 entry chunk
通过配置 optimization.runtimeChunk = true,为运行时代码创建一个额外的 chunk,减少 entry chunk 体积,提高性能;
webpack.prod.js 配置方式如下:
module.exports = {optimization: {runtimeChunk: true,},};}
效果:
5.3 Tree Shaking(摇树)
1个模块可能有多个⽅法,只要其中的某个方法使⽤到了,则整个⽂件都会被打到 bundle 里面去,tree shaking 就是只把⽤到的方法打入 bundle ,没⽤到的方法会在uglify阶段被擦除掉;
5.3.1 JS
JS Tree Shaking将 JavaScript 上下文中的未引用代码(Dead Code)移除,通过 package.json 的 “sideEffects” 属性作为标记,向 compiler 提供提示,表明项目中的哪些文件是 “pure(纯正 ES2015 模块)”,由此可以安全地删除文件中未使用的部分;
Dead Code 一般具有以下几个特征:
通过 package.json 的 “sideEffects” 属性,来实现这种方式;
{"name": "your-project","sideEffects": false}
需注意的是,当代码有副作用时,需要将 sideEffects 改为提供一个数组,添加有副作用代码的文件路径:
{"name": "your-project","sideEffects": ["./src/some-side-effectful-file.js"]}
Tree Shaking前:
Tree Shaking后:
2、对组件库引用的优化
webpack5 sideEffects 只能清除无副作用的引用,而有副作用的引用则只能通过优化引用方式来进行 Tree Shaking;
loadsh
类似 import { throttle } from ‘lodash’ 就属于有副作用的引用,会将整个 lodash 文件进行打包;
优化方式是使用 import { throttle } from ‘lodash-es’ 代替 import { throttle } from ‘lodash’, lodash-es将 Lodash库导出为 ES模块,支持基于 ES modules 的 tree shaking,实现按需引入;
ant-design
ant-design默认支持基于 ES modules 的 tree shaking,对于 js 部分,直接引入 import { Button } from ‘antd’ 就会有按需加载的效果;
假如项目中仅引入少部分组件,import { Button } from ‘antd’ 也属于有副作用,webpack不能把其他组件进行tree-shaking。这时可以缩小引用范围,将引入方式修改为 import { Button } from ‘antd/lib/button’ 来进一步优化。
5.3.2 CSS
使用 purgecss-webpack-plugin对 CSS Tree Shaking。
安装:
npm i purgecss-webpack-plugin -D
因为打包时 CSS 默认放在 JS 文件内,因此要结合 webpack 分离 CSS 文件插件 mini-css-extract-plugin 一起使用,先将 CSS 文件分离,再进行 CSS Tree Shaking;
webpack.prod.js 配置方式如下:
const glob = require('glob')const MiniCssExtractPlugin = require('mini-css-extract-plugin')const PurgeCSSPlugin = require('purgecss-webpack-plugin')const paths = require('paths')module.exports = {plugins: [// 打包体积分析new BundleAnalyzerPlugin(),// 提取 CSSnew MiniCssExtractPlugin({filename: "[name].css",}),// CSS Tree Shakingnew PurgeCSSPlugin({paths: glob.sync(`${paths.appSrc}/**/*`, { nodir: true }),}),]}
5.4 CDN
通过 CDN 来减小打包体积;
将大的静态资源上传至 CDN:
- 字体:压缩并上传至 CDN;
 - 图片:压缩并上传至 CDN。
 
6、加快加载速度
6.1 按需加载
通过 webpack 提供的 import() 语法动态导入功能进行代码分离,通过按需加载,大大提升网页加载速度;
export default function App () {return (<div>hello react 111<Hello /><button onClick={() => import('lodash')}>加载lodash</button></div>)}
效果如下:
6.2 浏览器缓存
浏览器缓存,就是进入某个网站后,加载的静态资源被浏览器缓存,再次进入该网站后,将直接拉取缓存资源,加快加载速度。
webpack 支持根据资源内容,创建 hash id,当资源内容发生变化时,将会创建新的 hash id。
配置 JS bundle hash,webpack.common.js 配置方式如下:
module.exports = {// 输出output: {// 仅在生产环境添加 hashfilename: ctx.isEnvProduction ? '[name].[contenthash].bundle.js' : '[name].bundle.js',},}
配置 CSS bundle hash,webpack.prod.js 配置方式如下:
module.exports = {plugins: [// 提取 CSSnew MiniCssExtractPlugin({filename: "[hash].[name].css",}),],}
配置 optimization.moduleIds,让公共包 splitChunks 的 hash 不因为新的依赖而改变,减少非必要的 hash 变动,webpack.prod.js 配置方式如下:
module.exports = {optimization: {moduleIds: 'deterministic',}}
通过配置 contenthash/hash,浏览器缓存了未改动的文件,仅重新加载有改动的文件,大大加快加载速度。
6.3 CDN
上面已经介绍过啦;
7、总结
在小型项目中,添加过多的优化配置,作用不大,反而会因为额外的 loader、plugin 增加构建时间;
在加快构建时间方面,作用最大的是配置 cache,可大大加快二次构建速度。
在减小打包体积方面,作用最大的是压缩代码、分离重复代码、Tree Shaking,可最大幅度减小打包体积。
在加快加载速度方面,按需加载、浏览器缓存、CDN 效果都很显著。
