背景:公司项目develop编译缓慢,在同时处理多需求、工单时,切换分支后需要等待较久时间,特此记录。
拉齐认知环境
硬件:
- 版本:Windows 10 专业版 22H2
- 处理器:12th Gen Intel(R) Core(TM) i7-1260P 2.10 GHz
- RAM:16.0 GB (15.7 GB 可用)
环境:
"webpack": "^4.44.2",
"webpack-cli": "^3.3.12",
"cache-loader": "^4.1.0",
"hard-source-webpack-plugin": "^0.13.1",
"speed-measure-webpack-plugin": "^1.5.0",
"node": "14.21.0", // 较稳定 - 推荐使用版本
"npm": "6.14.17"
打包工具:
webpack工作原理概括webpack运行流程是串行方式的,大致如下:
- 初始化参数:从配置文件和 Shell 语句中读取与合并参数配置,得出最终的参数配置;
- 开始编译:用上一步得到的参数配置初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译;
- 确定入口:根据配置中的 entry 找出所有的入口文件;
- 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤。直到所有入口依赖的文件都经过了本步骤的处理;
- 完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;
- 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;
- 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。
提示:在以上的过程中,webpack会在特定的时机广播出对应的事件,方便触发插件中监听了这些事件的钩子运行。 看似流程较多,我们可以精简一下:
- 初始化:启动构建,读取与合并配置参数,加载 Plugin,实例化 Compiler。
- 编译:从 Entry 发出,针对每个 Module 串行调用对应的 Loader 去翻译文件内容,再找到该 Module 依赖的 Module,递归地进行编译处理。
- 输出:对编译后的 Module 组合成 Chunk,把 Chunk 转换成文件,输出到文件系统。
统计数据工具:
打包工具插件:
Disk Cache(📈50%)
在一些性能开销较大的 loader 之前添加 cache-loader,将结果缓存中磁盘中。默认保存在 node_modueles/.cache/cache-loader 目录下。
HappyPack(📉10%)
由于有大量文件需要解析和处理,构建是文件读写和计算密集型的操作,特别是当文件数量变多后,Webpack 构建慢的问题会显得严重。文件读写和计算操作是无法避免的,那能不能让 Webpack 同一时刻处理多个任务,发挥多核 CPU 电脑的威力,以提升构建速度呢?
HappyPack 就能让 Webpack 做到这点,它把任务分解给多个子进程去并发的执行,子进程处理完后再把结果发送给主进程。
thread-loader(📉20%)
除了使用 Happypack 外,我们也可以使用 thread-loader ,把 thread-loader 放置在其它 loader 之前,那么放置在这个 loader 之后的 loader 就会在一个单独的 worker 池中运行。
在 worker 池(worker pool)中运行的 loader 是受到限制的。例如:
- 这些
loader不能产生新的文件。 - 这些
loader不能使用定制的loaderAPI(也就是说,通过插件)。 - 这些
loader无法获取webpack的选项设置。
HardSourceWebpackPlugin(存疑)(📈80%)
HardSourceWebpackPlugin 为模块提供中间缓存,缓存默认的存放路径是: node_modules/.cache/hard-source。
配置 hard-source-webpack-plugin,首次构建时间没有太大变化,但是第二次开始,构建时间大约可以节约 80%。
优化过程对比
截图中数据均为刨除异常数据,取中位值截图展示 由于编译中CPU与内存对结果的影响较大,所以展示数据 100% 有一定量的偏差,请理性看待
Disk Cache
未优化
使用Disk Cache 📈 54%
babel.cacheDirectory
开启 babel-loader 的缓存和配置 cache-loader,我比对了下,构建时间很接近。
Disk Cache + thread-loader
此处在使用CPU多核开启多线程中发现异常,无论是 happypack or thread-loader 均未正向优化,其中 thread-loader 📉10%,happypack 📉10%~30%
理论而言此类现象不正常,由于无法迁移公司代码至服务器中尝试,所以只能依靠经验做一些浅显判断
查看公司项目,📄文件 2400+ 43MB 源码量,对比之前经手项目📄文件 5600+ 170MB 源码量,在开启多核、多线程功能 develop 编译可以下降至 60000ms 以内
另外一个理论支撑点在于,查看 Node.js - Runtime 进程中发现,无论是否开启多核、多线程,**cpu使用率 **与 内存占有率对比未开启状态,均无明显提升
所以最终推测可能与 **计算机系统 **与 **硬件 **相关,所以不再深入探寻
HardSourceWebpackPlugin
使用 HardSourceWebpackPlugin后 第一次编译
使用 HardSourceWebpackPlugin后 第二次编译
[hardsource:f225e29c] Writing new cache f225e29c...
[hardsource:f225e29c] Tracking node dependencies with: package-lock.json, yarn.lock.
# 开启 HardSourceWebpackPlugin 后
# 在控制台看到如上输出 则说明 HardSourceWebpackPlugin 生效 并在 写入 缓存文件
[hardsource:f225e29c] Using 721 MB of disk space.
[hardsource:f225e29c] Tracking node dependencies with: package-lock.json, yarn.lock.
[hardsource:f225e29c] Reading from cache f225e29c...
# 在控制台看到如上输出 则说明 HardSourceWebpackPlugin 生效 并在 使用 缓存文件
HardSourceWebpackPlugin 是让我感到诧异的第二类优化方向,webpack中较核心的功能 - 持久化缓存
HardSourceWebpackPlugin 的实现原理是将编译后的文件,写入 node_modules/.cache/hard-source 文件中
如果在 npm 包的 lock 地图文件未发生变动时,会通过 MD5 比对缓存文件是否需要再次编译,否则直接复用,由于在 webpack 编译过程中最耗时的操作就是各种 loader,所以在此做优化会节省非常 **可观 **的时间
但在此次使用过程中,遇到 编译卡死、编译耗时夸张、缓存无法利用...等问题
查找网上资料后发现,webpack 的维护团队在 Issues 中的回答是:
- 建议升级Webpack5
- Node.js 版本 > 17
目前测试较稳定且能使用在公司项目中的 Node.js 版本是 14.21.0,但此版本存在初次编译耗时较长的问题,其余问题到未复现
由于 webpack5 中的 **持久化缓存方案 **已经内置,所以不在深入探寻
总结
Node.js 的版本迭代,打包工具的升级更新,每个版本都会大幅提升性能,及时升级工具也是提升构建效率的有效手段
另外在优化手段上需要注意,不可盲目的过多开启 loader 与 插件功能,因为 Node.js 的 runtime 和 loader 都有启动开销。要最小化 worker 和 main process(主进程) 之间的数据传输,因为进程间通讯(IPC, inter process communication)也是非常消耗资源的。