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: [
// 接入 DllPlugin
new 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
<script
src="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 chunks
chunks: 'all',
// 重复打包问题
cacheGroups:{
// node_modules里的代码
// 第三方模块
vendors:{
test: /[\\/]node_modules[\\/]/,
chunks: "all",
// name: 'vendors', 一定不要定义固定的name
priority: 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(),
// 提取 CSS
new MiniCssExtractPlugin({
filename: "[name].css",
}),
// CSS Tree Shaking
new 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: {
// 仅在生产环境添加 hash
filename: ctx.isEnvProduction ? '[name].[contenthash].bundle.js' : '[name].bundle.js',
},
}
配置 CSS bundle hash,webpack.prod.js 配置方式如下:
module.exports = {
plugins: [
// 提取 CSS
new 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 效果都很显著。