深海鱼的博客 深海鱼的博客
首页
  • 《ES6 教程》
  • 《TypeScript》
  • 《Vue》
  • 《React》
  • 《Git》
  • Javascript
  • CSS
手写系列
  • React项目实战
  • Vue3项目实战
  • 服务端项目实战
  • 鸿蒙项目实战
  • 小小实践
  • Vue全家桶

    • Vue2.0
    • Vue3.0
    • VueRouter
    • Vuex
  • React

    • React
  • Axios
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

深海鱼

尽人事,知天命,知行合一。
首页
  • 《ES6 教程》
  • 《TypeScript》
  • 《Vue》
  • 《React》
  • 《Git》
  • Javascript
  • CSS
手写系列
  • React项目实战
  • Vue3项目实战
  • 服务端项目实战
  • 鸿蒙项目实战
  • 小小实践
  • Vue全家桶

    • Vue2.0
    • Vue3.0
    • VueRouter
    • Vuex
  • React

    • React
  • Axios
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 实践练习

    • 基于React16的Webpack升级与构建速度优化
      • 1. 项目概况
      • 2.构建调试与分析
      • 3. 优化思路
      • 4. 优化
        • 开发环境启动优化
        • 生产环境打包优化
      • 总结
  • 全栈项目

  • 项目实战
  • 实践练习
深海鱼
2024-07-09
目录

基于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
1
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"
  }
}
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
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",
}
1
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({

})
1
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)
1
2
3
4
5
6

添加后分别执行如下脚本启动服务和构建,两个脚本都需要执行两次,以比较首次构建和二次构建的情况:

# 启动服务
yarn start

# 构建
yarn build
1
2
3
4
5

两次构建结果如下:

1. 开发环境启动服务耗时

start-detail

2. 生产环境构建耗时

build-detail

  • 开发环境首次启动耗时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版本或其它构建工具。基于这个思路,可以尝试以下优化方案:

  1. 在原有的脚手架上优化,保持所使用的loader和plugin不变,对相关loader和plugin的配置
  2. 将Webpack版本升级到5.x版本,并且按需移除和添加loader和plugin,以减少构建的开销。
  3. 将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
1
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
1
2
3

这里之所以没有安装file-loader、url-loader是因为在Webpack 5.x中,加载资源已经使用模块资源 (opens new window)替代了,不再需要这些loader。

升级以上内容后,执行启动命令看看:

yarn start
1

此时控制台报如下错误:

Error: Cannot find module 'url-loader'
1

如上文所说,静态资源的加载我们不再需要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]'
    }
  }
]
1
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]'
    },
  }
]
1
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.
1

(3)升级IgnorePlugin的配置

上面指的是webpack.IgnorePlugin的配置不对,这是因为在webpack 5.x中,IgnorePlugin的配置已经发生了变化,在升级指南中 (opens new window)告诉我们,配置已经改成了一个对象,需要做如下修改:

new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)
// 改成
new webpack.IgnorePlugin({
  resourceRegExp: /^\.\/locale$/,
  contextRegExp: /moment$/
})

1
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? }
1
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
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

运行,报如下错误:

configuration.optimization has an unknown property 'namedModules'.
1

(5)升级optimization配置

同样,升级指南里提到,在Webpack5,optimization.namedModules: true以及升级为 optimization.moduleIds: 'named',做如下修改:

optimization: {
  namedModules: true
}
// 改为
optimization: {
  moduleIds: 'named'
}
1
2
3
4
5
6
7

运行,报错误:

orig.plugin is not a function
1

(6)升级CaseSensitivePathsPlugin

以上错误看起来像是一个插件引起的错误,在上面的操作中,我们除了html-webpack-plugin插件升级了以后,其它的还未做处理,通过调试,发现此错误由CaseSensitivePathsPlugin报出,先升级该插件:

yarn remove case-sensitive-paths-webpack-plugin
yarn add case-sensitive-paths-webpack-plugin -D
1
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? }
1
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 => {
  // ...
});
1
2
3
4
5

WebpackDevServer接收的参数如下:

dev-server-options

可以看到,从v4版本开始WebpackDevServer接收的参数与旧版本的顺序是相反的,这个在webpack-dev-serverv3-v4升级文档中有说明。

  • v3-v4升级文档 (opens new window)
  • v4-v5升级文档 (opens new window)

后面还会从文档中查看其它的升级配置项

所以把WebpackDevServer实例化的代码修改如下:

const devServer = new WebpackDevServer(compiler, serverConfig)
// 改为
const devServer = new WebpackDevServer(serverConfig, compiler)
1
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.
1
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
}
1
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
}
1
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;
}
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

注意

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
1

这个错误从start.js文件调用devServer.listen报出来,通常查看升级文档,看到该api改成使用async devServer.start或startCallback代替。修改如下:

devServer.listen(port, HOST, err => {})
// 改为
devServer.startCallback(err => {})
1
2
3

同理后文的devServer.close()方法改成了devServer.stopCallback()

再次运行服务启动成功,但控制输出如下错误:

[webpack-dev-middleware] TypeError: message.split is not a function
1

(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
1
2

升级后运行提示

Error: Cannot find module 'react-dev-utils/WatchMissingNodeModulesPlugin'
1

这是因为react-dev-utils已经移除了WatchMissingNodeModulesPlugin这个方法,移除方法通常意味着这个方法不再需要,这可能是Webpack5和create-react-app升级后内部机制变化引起的。在webpack.config.dev.js移除该插件的配置即可。

运行后又报错了:

 configuration has an unknown property 'applyWebpackOptionsDefaults'. ....
1

🤮这个错误着实把我给整自闭了,经过一系列调试后发现,该版本的react-dev-utils/WebpackDevServerUtils中的createCompiler方法接收的是一个对象,而以前版本接收的是一个参数列表。打开start.js文件有如下代码:

const compiler = createCompiler( webpack, config, appName, urls, useYarn )
1

改成

const compiler = createCompiler({ webpack, config, appName, urls, useYarn })
1

修改后再启动,服务启动终于启动成功了。但是此时打开应用仍然是无法正常访问的,这是因为相关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"
    ]
  },
}
1
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 }]
  ]
}
1
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'
1
2

上面安装相关依赖时,漏装了这两个插件

yarn add babel-plugin-transform-decorators-legacy babel-preset-react-app -D
1

再启动,项目成功启动并正常运行。

(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
1
// .eslintrc.js
module.exports = {
  plugins: ['import'],
  rules: {
    'import/no-unresolved': [2, { caseSensitive: true }]
  }
}
1
2
3
4
5
6
7
// tsconfig.json
{
  "strict": true,
  "forceConsistentCasingInFileNames": true
}
1
2
3
4
5

处理方式:移除以上loader和插件的配置,增加cache配置:

{
  cache: {
    type: "filesystem",
    buildDependencies: {
      config: [__filename]
    },
    name: "dev_cache"
  }
}
1
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"
},
1
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
1
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()
    ]
  }
}

1
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

1
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
1
2
3
4
5
6
7
8
9
10

再次运行,效果如下:

new-build-1

可以看到,结果是1min+,相比优化前的10min+,现在已经节省了大量的打包时间。但我们也发现优化后首次打包和二次打包时间并没有显著减少,这是为什么呢?这很有可能是webpack5的cache配置的缓存没有起作用。原因要么是配置不正确,要么是配置正确但是某些其它的原因引起了配置没有生效。首先检查配置,在cache配置中,我们分别为开发环境和生产环境配置了不同的cache名称(dev_cache和build_cache),因此webpack5会为这两个环境分别生成不同的缓存文件,缓存的目录为node_modules/.cache/webpack/缓存名称。打开node_modules/.cache/webpack目录,可以看到dev_cache目录是存在的,而build_cache目录不存在。

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;
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

再执行编译,结果如下:

new-build-2

一下子就舒坦了有没有😊

  • 优化前,首次打包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等插件和loader
  • webpack.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
最近更新: 2024/07/12, 14:40
index

index→

最近更新
01
Axios源码解读
07-29
02
Vue-Router源码解读
07-09
03
Vue2.x源码解读
07-09
更多文章>
Theme by Vdoing | Copyright © 2024-2024 深海鱼 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式