基于React16的Webpack升级与构建速度优化
前几天,前端的同事说有个项目构建速度非常慢,开发效率和发布效率都非常低,甚至已经严重影响到团队之间的协作效率了。项目已经是老项目了,使用了较为老旧的技术栈,也没有条件重构到新一些的技术栈,所以问我有没有办法对该项目的打包速度做一次优化。虽然我自己没有参与过这个项目,但是我心里明白,这个项目就是传说中的屎山级别代码的项目,所以我内心其实是拒绝的。不过想到该同事可能已经做过努力尝试让项目打包速度提升但没有效果,所以我还是接下来了这个事,试试能否解决这个问题,哪怕只是提升一小部分的速度。
# 1. 项目概况
这是一个由Create-React-App
创建的项目React 16的项目,同时已经执行过eject
命令将项目的配置暴露出来了。项目的配置文件在scripts/config
目录下,其中核心的配置文件有如下几个:
scripts/start.js
:启动开发服务器的文件scripts/build.js
:打包的脚本文件scripts/config/webpack.config.dev.js
: 开发环境webpack配置文件scripts/config/webpackDevServer.config.js
: 开发环境webpack-dev-server配置文件scripts/config/webpack.config.prod.js
: 生产环境webpack配置文件
项目的目录结构如下:
├── README.md
├── package.json
├── public
├── scripts
│ ├── build.js
│ ├── config
│ ├── start.js
│ └── test.js
├── settings.json
├── src
└── yarn.lock
2
3
4
5
6
7
8
9
10
11
打开package.json
文件,看一下依赖(仅列出与webpack打包相关的依赖)和脚本:
package.json
{
"scripts": {
"start": "set NODE_OPTIONS=--openssl-legacy-provider && node --max-old-space-size=4000 scripts/start.js",
"build": "set NODE_OPTIONS=--openssl-legacy-provider && node --max-old-space-size=4000 scripts/build.js",
"build:local": "node --max-old-space-size=4000 scripts/build.js",
"build:dev": "set UPLOAD=true && node --max-old-space-size=4000 scripts/build.js --upload=dev",
"build:test1": "set UPLOAD=true && node --max-old-space-size=4000 scripts/build.js --upload=test1",
"build:test2": "set UPLOAD=true && node --max-old-space-size=4000 scripts/build.js --upload=test2",
"test": "node scripts/test.js --env=jsdom"
},
"dependencies": {
"autodll-webpack-plugin": "^0.4.2",
"autoprefixer": "7.1.6",
"babel-core": "6.26.0",
"babel-eslint": "7.2.3",
"babel-jest": "20.0.3",
"babel-loader": "7.1.2",
"babel-preset-react-app": "^3.1.1",
"babel-runtime": "6.26.0",
"cache-loader": "^4.1.0",
"caniuse-lite": "^1.0.30001211",
"case-sensitive-paths-webpack-plugin": "2.1.1",
"css-loader": "0.28.7",
"eslint": "4.10.0",
"eslint-config-react-app": "^2.1.0",
"eslint-loader": "2.1.0",
"eslint-plugin-flowtype": "2.39.1",
"eslint-plugin-html": "^6.0.0",
"eslint-plugin-import": "2.8.0",
"eslint-plugin-jsx-a11y": "5.1.1",
"eslint-plugin-react": "^7.14.3",
"file-loader": "1.1.11",
"html-webpack-plugin": "4.5.0",
"mini-css-extract-plugin": "1.0.0",
"optimize-css-assets-webpack-plugin": "5.0.3",
"postcss-flexbugs-fixes": "3.2.0",
"postcss-loader": "2.0.8",
"postcss-normalize": "8.0.1",
"postcss-preset-env": "6.7.0",
"postcss-safe-parser": "4.0.1",
"sftp-uploader": "^1.0.0",
"speed-measure-webpack-plugin": "^1.3.3",
"style-loader": "0.19.0",
"thread-loader": "^3.0.4",
"uglifyjs-webpack-plugin": "^2.2.0",
"url-loader": "0.6.2",
"webpack": "4.41.2",
"webpack-cli": "3.1.0",
"webpack-dev-server": "3.11.0",
"whatwg-fetch": "2.0.3",
"worker-loader": "^3.0.5",
"xml-loader": "^1.2.1"
},
"devDependencies": {
"babel-plugin-transform-decorators-legacy": "^1.3.4",
"hard-source-webpack-plugin": "^0.13.1",
"less": "^2.7.3",
"less-loader": "^4.0.5",
"react-hot-loader": "^4.11.0",
"terser-webpack-plugin": "^4.1.0"
}
}
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
虽然DevDependencies
和dependencies
的管理混乱,但从中我们可以知道以下信息:
- webpack使用的是4.x版本
- webpack-cli使用的是3.x版本
- webpack-dev-server使用的是3.x版本
- 各种loader和plugin使用的版本参差不齐,但总体是属于较低的版本或废弃的版本
实际调试过程还发现,有些loader和plugin与所使用的webpack版本是不匹配的,而虽然webpack使用的是4.x的版本,但是构建的配置有一部分其实还是用的3.x的版本,所以需要做兼容处理,项目的混乱程度可见一斑,不愧是祖传屎山😂😂
# 2.构建调试与分析
基于现有的脚手架依赖,先来看看目前的构建情况,这个过程需要借助以下speed-measure-webpack-plugin
插件来获取构建的耗时情况。从package.json
中可以看到已经添加过此插件的依赖,所以无需再重新安装:
"dependencies": {
"speed-measure-webpack-plugin": "^1.3.3",
}
2
3
以下分别从开发环境启动和生产环境构建两个地方来测试下构建的情况。
打开scripts/config/webpack.config.dev.js
和scripts/config/webpack.config.prod.js
文件,分别添加如下代码:
// webpack.config.dev.js
//....
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin")
const smp = new SpeedMeasurePlugin()
// ...
module.exports = smp.wrap({
})
2
3
4
5
6
7
8
// webpack.config.prod.js
//....
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin")
const smp = new SpeedMeasurePlugin()
// ...
module.exports = smp.wrap(webpackConfig)
2
3
4
5
6
添加后分别执行如下脚本启动服务和构建,两个脚本都需要执行两次,以比较首次构建和二次构建的情况:
# 启动服务
yarn start
# 构建
yarn build
2
3
4
5
两次构建结果如下:
1. 开发环境启动服务耗时
2. 生产环境构建耗时
- 开发环境首次启动耗时4min,28.21s,二次启动耗时2min,12.97s
- 生产环境首次构建耗时11min,2.23s,二次构建耗时1min,25.048s
虽然这个项目不是大型项目,但也不算太小的项目。从二次启动和构建结果来看,其实表现还算OK,也没有同事反映的问题那么严重🤣。通过speed-measure-webpack-plugin
计算出来的结果,可以知道,打包的瓶颈主要在以下loader和plugin上面。
- 开发环境启动
cache-loader
+babel-loader
+thread-loader
less-loader
+css-loader
+cache-loader
CaseSensitivePathsPlugin
IgnorePlugin
HotModuleReplacementPlugin
- 生产环境构建
less-loader
+poctcss-loader
+css-loader
+cache-loader
cache-loader
+babel-loader
+thread-loader
HardSourceWebpackPlugin
(10+min、31+s)IgnorePlugin
TerserPlugin
OptimizeCSSAssetsPlugin
(21+s)UglifyJsPlugin
create-react-app
脚手架已经帮我们做了很多构建方面的优化,包括cache-loader
、thread-loader
、HardSourceWebpackPlugin
、webpack.IgnorePlugin
、TerserPlugin
、OptimizeCSSAssetsPlugin
、UglifyJsPlugin
等。这个脚手架足以在中大型项目中解决绝大部分的构建问题。然而,这些优化使用的loaders
和plugins
本身也是有开销的,这在有些项目可能导致收益小于额外开销的结果。从上面的数据中可以初步判断,像cache-loader
和HardSourceWebpackPlugin
这样的插件或者loader在本项目的收益是比较有限的。因此,我们根据项目的实际情况,对这些插件和loader进行优化,以减少构建的耗时。
# 3. 优化思路
通过上面的分析,优化的基本思路应该是减少loaders
和plugins
的构建开销。首先应该清楚,构建工具跟项目中使用的框架通常是没有什么必然联系的,这意味同一个项目,既可以选择Webpack
,也可以选择Vite
。因此,虽然该用了React
叫老旧的版本,但是其仍然可以使用较高版本的Webpack
版本或其它构建工具。基于这个思路,可以尝试以下优化方案:
- 在原有的脚手架上优化,保持所使用的
loader
和plugin
不变,对相关loader
和plugin
的配置 - 将
Webpack
版本升级到5.x版本,并且按需移除和添加loader
和plugin
,以减少构建的开销。 - 将
Webpack
迁移到Vite
等其它构建
考虑到项目使用create-react-app
且所有的启动和构建脚本都是基于Webpack
来实现的,而且项目难以升级到较高版本的React
,同时Webpack 5.x
在性能上有所提高,所以本次优化采用方案2,通过将Webpack
版本升级到5.x版本,并且按需移除和添加loader
和plugin
,从而达到优化的目的。
# 4. 优化
从这一小节开始,我们先从开发环境启动再到生产环境构建,一步步的将Webpack
升级到5.x的版本。所有的依赖,采用先删除旧的依赖,然后安装新版本的方式,逐步调试构建结果。
# 开发环境启动优化
(1)升级webpack
版本:
yarn remove webpack webpack-cli webpack-dev-server
yarn add webpack webpack-cli webpack-dev-server -D
2
(2)升级核心的loader
和plugin
在项目中,最基础要处理的文件类型有js、jsx、less、css、html以及图片等静态资源,将处理这些文件的loader
和plugin
升级。
yarn remove less less-loader css-loader style-loader mini-css-extract-plugin html-webpack-plugin file-loader url-loader babel-loader babel-core babel-preset-react-app babel-runtime babel-plugin-transform-decorators-legacy
yarn add less less-loader css-loader style-loader mini-css-extract-plugin html-webpack-plugin babel-loader @babel/core @babel/preset-react @babel/preset-env @babel/runtime @babel/plugin-transform-runtime @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators
2
3
这里之所以没有安装file-loader
、url-loader
是因为在Webpack 5.x
中,加载资源已经使用模块资源 (opens new window)替代了,不再需要这些loader。
升级以上内容后,执行启动命令看看:
yarn start
此时控制台报如下错误:
Error: Cannot find module 'url-loader'
如上文所说,静态资源的加载我们不再需要url-loader
和file-loader
,需要改成Webpack5模块加载的方式。打开webpack.config.dev.js
文件,修改相关loader配置:
oneOf: [
{
test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
use: [
'cache-loader',
{
loader: require.resolve('url-loader'),
options: {
limit: 10000,
name: 'static/media/[name].[hash:8].[ext]'
}
}
]
},
{
exclude: [/\.(js|jsx|mjs)$/, /\.html$/, /\.json$/, /\.(css|less)$/],
loader: require.resolve('file-loader'),
options: {
name: 'static/media/[name].[hash:8].[ext]'
}
}
]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
更改为:
oneOf: [
{
test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
type: 'asset/resource',
generator: {
filename: 'static/media/[name].[hash:8].[ext]'
},
parser: {
dataUrlCondition: {
maxSize: 10000
}
}
},
{
exclude: [/\.(js|jsx|mjs)$/, /\.html$/, /\.json$/, /\.(css|less)$/],
type: "asset/resource"
generator: {
filename: 'static/media/[name].[hash:8].[ext]'
},
}
]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
再次运行,报如下错误:
ValidationError: Invalid options object. Ignore Plugin has been initialized using an options object that does not match the API schema.
(3)升级IgnorePlugin
的配置
上面指的是webpack.IgnorePlugin
的配置不对,这是因为在webpack 5.x
中,IgnorePlugin
的配置已经发生了变化,在升级指南中 (opens new window)告诉我们,配置已经改成了一个对象,需要做如下修改:
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)
// 改成
new webpack.IgnorePlugin({
resourceRegExp: /^\.\/locale$/,
contextRegExp: /moment$/
})
2
3
4
5
6
7
运行,报如下错误:
Invalid configuration object. Webpack has been initialized using a configuration object that does not match the API schema.
- configuration.node should be one of these:
false | object { __dirname?, __filename?, global? }
-> Include polyfills or mocks for various node stuff.
Details:
* configuration.node has an unknown property 'dgram'. These properties are valid:
object { __dirname?, __filename?, global? }
-> Options object for node compatibility features.
* configuration.node has an unknown property 'fs'. These properties are valid:
object { __dirname?, __filename?, global? }
-> Options object for node compatibility features.
* configuration.node has an unknown property 'net'. These properties are valid:
object { __dirname?, __filename?, global? }
-> Options object for node compatibility features.
* configuration.node has an unknown property 'tls'. These properties are valid:
object { __dirname?, __filename?, global? }
-> Options object for node compatibility features.
* configuration.node has an unknown property 'child_process'. These properties are valid:
object { __dirname?, __filename?, global? }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
(4)升级node
配置
以上错误也是Webpack5中node
配置发生了变化,在升级指南中 (opens new window)也有说明:如果使用了类似于node.fs: 'empty'
,请使用resolve.fallback.fs: false
代替。
// 删除以下配置
node: {
dgram: 'empty',
fs: 'empty',
net: 'empty',
tls: 'empty',
child_process: 'empty'
},
// 增加如下配置:
resolve: {
fallback: {
dgram: false,
fs: false,
net: false,
tls: false,
child_process: false
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
运行,报如下错误:
configuration.optimization has an unknown property 'namedModules'.
(5)升级optimization
配置
同样,升级指南里提到,在Webpack5
,optimization.namedModules: true
以及升级为 optimization.moduleIds: 'named'
,做如下修改:
optimization: {
namedModules: true
}
// 改为
optimization: {
moduleIds: 'named'
}
2
3
4
5
6
7
运行,报错误:
orig.plugin is not a function
(6)升级CaseSensitivePathsPlugin
以上错误看起来像是一个插件引起的错误,在上面的操作中,我们除了html-webpack-plugin
插件升级了以后,其它的还未做处理,通过调试,发现此错误由CaseSensitivePathsPlugin
报出,先升级该插件:
yarn remove case-sensitive-paths-webpack-plugin
yarn add case-sensitive-paths-webpack-plugin -D
2
运行,发现该错误消失了,报了新的错误如下:
Invalid options object. Dev Server has been initialized using an options object that does not match the API schema.
- options has an unknown property '_assetEmittingPreviousFiles'. These properties are valid:
object { allowedHosts?, bonjour?, client?, compress?, devMiddleware?, headers?, historyApiFallback?, host?, hot?, ipc?, liveReload?, onListening?, open?, port?, proxy?, server?, setupExitSignals?, setupMiddlewares?, static?, watchFiles?, webSocketServer? }
2
3
(7)升级webpack-dev-server
的配置
以上错误信息看起来由webpack-dev-server
报出,打开webpackDevServer.config.js
文件,查看相关配置信息。可以看到配置中并没有_assetEmittingPreviousFiles
这个配置项。通过定位找到当前的webpack-dev-server
被文件start.js
引用:
const devServer = new WebpackDevServer(compiler, serverConfig)
// Launch WebpackDevServer.
devServer.listen(port, HOST, err => {
// ...
});
2
3
4
5
WebpackDevServer
接收的参数如下:
可以看到,从v4版本开始WebpackDevServer
接收的参数与旧版本的顺序是相反的,这个在webpack-dev-server
v3-v4升级文档中有说明。
后面还会从文档中查看其它的升级配置项
所以把WebpackDevServer
实例化的代码修改如下:
const devServer = new WebpackDevServer(compiler, serverConfig)
// 改为
const devServer = new WebpackDevServer(serverConfig, compiler)
2
3
运行,报错误:
options has an unknown property 'before'. These properties are valid:
object { allowedHosts?, bonjour?, client?, compress?, devMiddleware?, headers?, historyApiFallback?, host?, hot?, ipc?, liveReload?, onListening?, open?, port?, proxy?, server?, setupExitSignals?, setupMiddlewares?, static?, watchFiles?, webSocketServer? }
- options.proxy should be an array:
-> Allows to proxy requests, can be useful when you have a separate API backend development server and you want to send API requests on the same domain.
2
3
4
options.proxy
只能接收一个数组了before
选项不存在
查看以上升级文档,发现
- 从5.0版本开始,
proxy
配置只接收一个对象数组,旧版的对象中的key转为数组项中的context
属性 before
选项被移除,在v4版本onBeforeSetupMiddleware
代替,在v5版本又被改成了setupMiddlewares
。
按照指南做如下修改:
打开config/proxyConfig.js
文件,修改如下:
const webpackProxyConfig = {
'/kepler/': {
'target': 'http://p.kuaidi100.com/',
'changeOrigin': true
},
//...
}
//...
module.exports = {
webpackProxyConfig,
refererProxyConfig
}
2
3
4
5
6
7
8
9
10
11
12
改成:
const webpackProxyConfig = {
'/kepler/': {
'target': 'http://p.kuaidi100.com/',
'changeOrigin': true
},
//...
}
//...
const webpackProxyConfigArray = []
for (let i in webpackProxyConfig) {
webpackProxyConfigArray.push({
context: [i],
...webpackProxyConfig[i]
})
}
module.exports = {
webpackProxyConfig: webpackProxyConfigArray,
refererProxyConfig
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
打开config/webpackDevServer.config.js
文件,修改before
配置:
before(app) {
app.use(errorOverlayMiddleware())
app.use(noopServiceWorkerMiddleware())
refererProxyConfig.forEach(item => {
//...
})
}
setupMiddlewares(middlewares, devServer) {
if (!devServer) {
throw new Error('webpack-dev-server is not defined');
}
middlewares.unshift(noopServiceWorkerMiddleware("/"))
middlewares.unshift(errorOverlayMiddleware())
refererProxyConfig.forEach(item => {
middlewares.unshift({
path: item.url,
middleware: (req, res) => {
getRawBody(req).then(buf => {
//....
const path = req.url
// 修改为
const path = req.originalUrl
})
}
})
})
return middlewares;
}
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
注意
req.url
的使用是不安全的,它在经过各种中间件的处理后可能会被改变,要使用req.originalUrl
接着运行后会陆续报出相关配置项错误的提示,按照提示,结合升级指南修改配置直至所有配置成功,主要有以下配置:
public
: 迁移至client.webSocketURL
overlay
: 迁移至client.overlay
https:true
: 迁移至server.type: https
watchOptions
: 迁移至static.watch
quite
: 移除该选项publicPath
: 迁移至devMiddleware.publicPath
watchContentBase
: 移除该选项,使用static.watch
代替contentBase
: 迁移至static.directory
clientLogLevel
: 移除该选项,使用client.logging
代替disableHostCheck
: 移除该选项,为true
时使用allowedHosts: 'all'
代替
修改完后运行报如下错误:
devServer.listen is not a function
这个错误从start.js
文件调用devServer.listen
报出来,通常查看升级文档,看到该api改成使用async devServer.start
或startCallback
代替。修改如下:
devServer.listen(port, HOST, err => {})
// 改为
devServer.startCallback(err => {})
2
3
同理后文的devServer.close()
方法改成了devServer.stopCallback()
再次运行服务启动成功,但控制输出如下错误:
[webpack-dev-middleware] TypeError: message.split is not a function
(8)升级react-dev-utils
通过相关资料查询,以上错误信息是react-dev-utils
在Webpack5
中的一个兼容性bug,在后续的版本中已经修复了该bug,因此对react-dev-utils
进行升级即可。Bug详情 (opens new window)
yarn remove react-dev-utils
yarn add react-dev-utils -D
2
升级后运行提示
Error: Cannot find module 'react-dev-utils/WatchMissingNodeModulesPlugin'
这是因为react-dev-utils
已经移除了WatchMissingNodeModulesPlugin
这个方法,移除方法通常意味着这个方法不再需要,这可能是Webpack5
和create-react-app
升级后内部机制变化引起的。在webpack.config.dev.js
移除该插件的配置即可。
运行后又报错了:
configuration has an unknown property 'applyWebpackOptionsDefaults'. ....
🤮这个错误着实把我给整自闭了,经过一系列调试后发现,该版本的react-dev-utils/WebpackDevServerUtils
中的createCompiler
方法接收的是一个对象,而以前版本接收的是一个参数列表。打开start.js
文件有如下代码:
const compiler = createCompiler( webpack, config, appName, urls, useYarn )
改成
const compiler = createCompiler({ webpack, config, appName, urls, useYarn })
修改后再启动,服务启动终于启动成功了。但是此时打开应用仍然是无法正常访问的,这是因为相关loader升级后还未进行正确的配置。下面就开始处理loader相关的问题。
(9)babel-loader处理
修改config/webpack.config.dev.js
和package.json
文件,babel-loader
的相关配置改成如下:
// config/webpack.config.dev.js
{
test: /\.(js|jsx|mjs)$/,
include: paths.appSrc,
use: [
'cache-loader',
'thread-loader',
'babel-loader?cacheDirectory=true'
]
}
// package.json
{
"babel": {
"presets": [
"react-app"
],
"plugins": [
"transform-decorators-legacy"
]
},
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
删除package.json
上的配置,在项目根目录下创建babel.config.js
文件,内容如下:
module.exports = {
presets: [
['@babel/preset-env', {
targets: '> 0.25%, not dead'
}],
'@babel/preset-react'
],
plugins: [
["@babel/plugin-transform-runtime"],
["@babel/plugin-proposal-decorators", { "legacy": true }],
["@babel/plugin-proposal-class-properties", { "loose": true }]
]
}
2
3
4
5
6
7
8
9
10
11
12
13
运行,此时控制台会报错误:
Cannot find package 'babel-plugin-transform-decorators-legacy'
Cannot find package 'babel-preset-react-app'
2
上面安装相关依赖时,漏装了这两个插件
yarn add babel-plugin-transform-decorators-legacy babel-preset-react-app -D
再启动,项目成功启动并正常运行。
(10)其它webpack配置项处理
webpack5
升级后,有些选项或插件已经不再手动指定,检查webpack.config.dev.js
文件,将这些选项移除或修改。
hash
改成fullhash
代替webpack.HotModuleReplacementPlugin
不再需要,改成devServer.hot:true
react-dev-utils/webpackHotDevClient
不再需要,删除
到此为止,关于webpack5
的升级的部分就已经处理完成了,但是此时启动的话,启动时间并没有显著降低,换句话说单纯升级到webpack5
在本项目中的效果并不显著。
(11)启动优化
从上文首次和二次启动的信息可以看出,cache-loader
、webpack.IgnorePlugin
、和CaseSensitivePathsPlugin
为主要的耗时插件和loader。这些插件和loader本身就是为了优化打包而存在。然而,它们在此项目带来的优化效果并不明显并无法覆盖自身的损耗。所以我们应该着重优化这几项。
首先是cache-loader
,它会将打包编译过程的内容做缓存,从而在二次编译的时候不会重复编译,在处理缓存的过程中,cache-loader
自身也是有时间开销的。而且据了解cache-loader
的缓存机制不是完全可靠的。在webpack5
中已经自带了cache
选项 (opens new window),因此可以通过配置cache
选项来代替cache-loader
。
其次是webpack.IgnorePlugin
,其是用来阻止某些模块被打包的,在webpack5
中,比如moment
的语言包模块。但是webpack.IgnorePlugin
的使用让开发环境启动变慢。在开发环境中,我们注重给的是打包效率,其实可不使用该插件。另外,从配置文件看,已经将moment
配置到了externals
中,因此可以移除该插件。
最后是CaseSensitivePathsPlugin
,这个插件会拖累应用整体的编译性能,其实社区上很多库都不再使用该插件了,如umi/cra/next.js,该插件完全可以使用eslint-plugin-import插件的no-unresolved
配置以及tsconfig.json的forceConsistentCasingInFileNames
配置结合IDE的能力项来代替:
yarn add eslint-plugin-import -D
// .eslintrc.js
module.exports = {
plugins: ['import'],
rules: {
'import/no-unresolved': [2, { caseSensitive: true }]
}
}
2
3
4
5
6
7
// tsconfig.json
{
"strict": true,
"forceConsistentCasingInFileNames": true
}
2
3
4
5
处理方式:移除以上loader和插件的配置,增加cache
配置:
{
cache: {
type: "filesystem",
buildDependencies: {
config: [__filename]
},
name: "dev_cache"
}
}
2
3
4
5
6
7
8
9
处理后,删除所有的缓存,启动项目,结果如下:
- 优化前,首次启动4min,28.21s,优化后1min,1.81s,时间减少了77%,性能提为原来的4.4倍
- 优化前,二次启动2min,12.97s,优化后7.21s,时间减少了95%,性能提升为原来的18.4倍
# 生产环境打包优化
做完开发环境优化后,Webpack5
的升级工作也都全部完成了,生产环境打包优化就比较简单了。
(1)同步与开发环境一致的配置
打开webpack.config.prod.js
文件,查看所有配置,参考开发环境的配置,将生产环境与开发环境一致的配置同步。主要有以下:
- 移除
cache-loader
- 修改
[hash]
为[fullhash]
或[contenthash]
- 移除
url-loader
、file-loader
,修改为type: asset/resource
- 将
node
配置移除,增加resolve.fallback
配置 - 移除
webpack.IgnorePlugin
插件 - 增加
cache
配置:
cache: {
type: "filesystem",
buildDependencies: {
config: [__filename]
},
name: "build_cache"
},
2
3
4
5
6
7
(2)移除HardSourceWebpackPlugin
从上文的打包分析中知道,,HardSourceWebpackPlugin
在打包时会耗费大量的时间,该插件为模块提供之间缓存,可以使第二次构建节省大量的时间。首次和二次打包的结果来看,二次打包确实比首次打包节省了不少时间。但是在本项目中的二次打包依然耗费了不少时间。Webpack5
已经内置了cache
配置,因此在这个项目中可以考虑将HardSourceWebpackPlugin
移除。
(3)移除TerserPlugin
和UglifyJsPlugin
TerserPlugin
和UglifyJsPlugin
都是压缩代码的插件,Webpack5
中已经内置了terser-webpack-plugin
,不再需要UglifyJsPlugin
。同时内置的TerserPlugin
的默认配置通常使用于大部分的应用。因此可以移除这两个插件。
(4)移除OptimizeCSSAssetsPlugin
OptimizeCSSAssetsPlugin
是用于压缩优化CSS的插件,在其官网中已经提示在Webpack5
中使用css-minimizer-webpack-plugin
来代替。
移除optimize-css-assets-webpack-plugin
插件,并添加css-minimizer-webpack-plugin
插件:
yarn remove optimize-css-assets-webpack-plugin
yarn add css-minimizer-webpack-plugin -D
2
对配置文件做如下修改:
// webpack.config.prod.js
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
{
optimization: {
minimizer: [
new OptimizeCSSAssetsPlugin({
// ...
})
]
}
}
// 修改为
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin")
{
optimization: {
minimizer: [
new CssMinimizerPlugin()
]
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
*(5)修改打包脚本
做完以上优化和修改,运行打包命令yarn build
,会报错:
Error: You forgot to add 'mini-css-extract-plugin' plugin (i.e. `{ plugins: [new MiniCssExtractPlugin()] }`), please read https://github.com/webpack-contrib/mini-css-extract-plugin#getting-started
2
这个是mini-css-extract-plugin
插件与speed-measure-webpack-plugin
插件不兼容引起的,可以先把mini-css-extract-plugin
插件的配置,放到smp.wrap(config)
之后来解决这个问题。
module.exports = smp.wrap(webpackConfig)
// 改成
const config = smp.wrap(webpackConfig)
config.plugins.push(new MiniCssExtractPlugin({
filename: 'static/css/[name].[contenthash:7].css',
chunkFilename: 'static/css/[name].[contenthash:7].css',
ignoreOrder: true
}))
module.exports = config
2
3
4
5
6
7
8
9
10
再次运行,效果如下:
可以看到,结果是1min+,相比优化前的10min+,现在已经节省了大量的打包时间。但我们也发现优化后首次打包和二次打包时间并没有显著减少,这是为什么呢?这很有可能是webpack5
的cache
配置的缓存没有起作用。原因要么是配置不正确,要么是配置正确但是某些其它的原因引起了配置没有生效。首先检查配置,在cache
配置中,我们分别为开发环境和生产环境配置了不同的cache
名称(dev_cache
和build_cache
),因此webpack5
会为这两个环境分别生成不同的缓存文件,缓存的目录为node_modules/.cache/webpack/缓存名称
。打开node_modules/.cache/webpack
目录,可以看到dev_cache
目录是存在的,而build_cache
目录不存在。
所以可以推测,配置本身是没有问题的,那就是其它原因引起的缓存配置失效。通过搜索资料找到如下资料:
- Webpack5 File caching cannot be used in production mode with custom compilers (opens new window)
- fix: production mode cache invalid (opens new window)
资料显示,webpack5
在自定义的compiler中,如果要cache生效,在调用compiler.run
后还要调用compiler.close
。恰巧create-react-app
老版本中没有调用compiler.close
,所以webpack5
的cache配置没有生效。
打开scripts/build.js
文件,在compiler.run
后添加compiler.close
:
function build(previousFileSizes) {
console.log('Creating an optimized production build...');
let compiler = webpack(config);
return new Promise((resolve, reject) => {
compiler.run((err, stats) => {
});
}).then(res => {
compiler.close(closeErr => {
closeErr && console.log(closeErr);
})
return res;
});
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
再执行编译,结果如下:
一下子就舒坦了有没有😊
- 优化前,首次打包11min,2.23s,优化后37.07s,时间减少了94.4%,性能提升为原来的17.9倍
- 优化前,二次打包1min,25.048s,优化后5.96s,时间减少了93%,提升提升了14.3倍
将优化后的项目在同事电脑跑一遍,数据结果如下:
- 首次启动54s,二次启动2.6s
- 首次打包43s,二次打包2s
不得不说我这老、旧、破(2014年)台式机已经不行了啊~
到这里,开发环境和生产环境的打包优化就基本都做完了,最后对dependencies
和devDependencies
做一下整理,就可以收工。
# 总结
本次实践其实仅仅只是做了webpack打包速度的优化,如果留意打包的结果会发现,其实打包出来的文件还有不少大文件,对打包结果做优化也是一项比较重要的工作。后续的文章,我再讲讲优化打包结果的相关的内容。
本文实践我,可以总结出如下一些内容:
Webpack5
整体性能比Webpack4
有所提升,但是需要配合相关的配置来实现create-react-app
脚手架默认的webpack打包配置本身已经做了非常多的优化,但是并不是适合所有的项目,可以根据项目的实际需求做调整cache-loader
,hard-source-webpack-plugin
等插件和loader可以给webpack提供打包内容的缓存,从而提高二次打包的速度,但是这些工具本身也是有性能开销的,在使用时可以权衡其开销和带来的成效,选择适当的使用方式webpack5
内置了cache
配置,从某种程度上可以替换cache-loader
,hard-source-webpack-plugin
等插件和loaderwebpack.IgnorePlugin
插件可以用来忽略不需要模块打包,减少打包体积,但是也有很大的性能开销,应该视具体情况来选择。本项目用来忽略moment
语言包,其实可以升级moment版本,因为高版本的moment支持按需导入。另外,在项目中已经配置了externals
,可以移除该插件配置case-sensitive-paths-webpack-plugin
可以避免不同操作系统下大小写敏感的问题,但是其性能开销非常大,可以使用eslint-plugin-import
插件的no-unresolved
规则以及tsconfig.json
的forceConsistentCasingInFileNames
配置,结合IDE的能力来替代Webpack5
内置了压缩插件,可以替换uglifyjs-webpack-plugin
,optimize-css-assets-webpack-plugin
等插件webpack5
内置了静态资源的加载处理,可以不再需要file-loader
和url-loader
等loader