Skip to content

记录一次vite项目build优化过程

问题描述

在项目打包时,会报错 out of memory 然后终止运行。这个错误不是必现的。

按照 vite issue 中提到的方法,以前已经通过配置 "build": "cross-env NODE_OPTIONS=--max-old-space-size=8192 vite build", 临时避免了此问题,可这次再次出现。

问题分析

表面原因

为了正确定位原因,进行了多次打包测试。

主要是切换不同Node版本、主要依赖版本、代码版本(因为是本次合并后才出现的此问题)。

测试结果如下:

通过上面的表格可以发现,不包含 @antv/g6 包的代码 build 耗时更短。

虽然内存和耗时是两个概念,但仍能反应一些问题。

提示

如果想要查看在 build 时的内存变化,这里提供两种方式:

  1. 使用 --inspect 参数:

    • 例如:"build": "node --inspect --max_old_space_size=16384 ./node_modules/vite/bin/vite.js build --mode deploy"
    • 浏览器进入 chrome://inspect,打开监控界面。
  2. 使用 pm2 执行打包命令:

    • 使用 pm2 运行 yarn build:test
      • pm2 start yarn --name "build" --interpreter bash --no-autorestart -- run build
      • 这将启动一个名为 buildPM2 进程,使用 bash 解释器来运行 yarn 命令,并在执行成功后退出。--no-autorestart 标志将防止进程在崩溃时自动重新启动。
    • 通过 pm2 monit 命令可以调出监控界面。

根本原因

造成 vite build 缓慢/内存消耗的根本原因在于项目中使用了 @vitejs/plugin-legacy 插件进行低版本浏览器的代码兼容造成的。

经过测试,同样的代码:

  • 在开启了该插件的情况下:打包耗时如上图在 470s 左右。
  • 在关闭了该插件的情况下:打包耗时如上图在 100s 左右。

可以看出差距是巨大的。

但是很明显对低版本浏览器的兼容是一项必须项。

解决问题

一开始的思路是通过配置 vite.configoptimizeDeps.exclude 来使 vite 不再分析 @antv/g6,经过测试发现并未生效。

WARNING

@vitejs/plugin-legacy 会遍历项目中的所有 JavaScript 模块文件,将其中的 ES6+ 语法转换为 ES5 语法,并将其输出到一个单独的文件中。因此,它不会关心 optimizeDeps.exclude 配置是否对某些文件生效。

如果你想避免 @vitejs/plugin-legacy 对某些模块进行编译:

  • html 中可以直接将这些模块标记为 nomodule
  • import() 动态导入的方式来加载该库

在这里我们显然采用了动态加载的方式导入该库。修改的地方:

ts
// *.vue
const initG6 = async () => {
    const G6 = await import('@antv/g6')
    ...
}

// vite.config.ts
export default defineConfig(({mode}) => ({
    resolve: {
        alias: {
            '@antv/g6': resolve(__dirname, 'node_modules/@antv/g6/dist/g6.min.js'),
        },
    },
})

为了使用时的方便以及统一配置的好处,我们使用别名的方式引入 node_modules/@antv/g6/dist/g6.min.js

尽管这样做解决了这个问题(节省服务器打包使用内存),因此也就要接受它的弊端,因为绕过了代码解析这一步骤也就缺少了对导入的模块进行代码处理(例如 TreeShake、Babel 转换、类型检查等)。

import()兼容性

  • Chrome >= 63
  • Edge >= 79
  • Safari >= 11.1
  • FireFox >= 67
  • Opera >= 50
  • IE 不支持

其他思路

类似的还有一些其他的解决方案,在这里记录下:

  • 同样的实现思路(都是避免vite编译),使用 cdn 加载,相应插件 vite-plugin-cdn-import。但这要求要在外网环境下使用。
  • 本想写一个类似于 vite-plugin-cdn-importvite-plugin 用于把引用的 node_modules/*/lib/** 转换为 node_modules/*/dist/*.min.js,后来醒悟这其实就是alias提供的能力...

相关

问题再次出现

在稳定运行打包很久之后,运维把 Jenkins 升级重新部署了,再次出现此问题(怀疑是把Jenkins内存配置减少了,或者新版本Jenkins自身占用内存变多了,导致程序运行的可用内存减少)。


通过Rollup官网可以看到,之所以会报错 JavaScript heap out of memory,其主要原因如 Rollup - Error: JavaScript heap out of memory 所说:

由于 Rollup 需要同时将所有模块信息保存在内存中,以便分析除屑优化(Tree-Shaking)的相关副作用,因此打包大型项目可能会达到 Node 的内存限制。

官网也给出了解决方案:

  1. 按需增加 --max-old-space-size 。请注意,这个数字可以安全地超过你的可用物理内存。在这种情况下,Node 会根据需要将内存分页到磁盘上。
  2. 你可以通过使用动态导入引入代码分割、只导入特定的模块而不是整个依赖、禁用 sourcemap,或者增加交换空间的大小来减少内存压力。

循环依赖

使用新版本 vite 构建时会报警告信息(其实是上游 Rollup 发出的警告):

警告信息

while both modules are dependencies of each other and will end up in different chunks by current Rollup settings. This scenario is not well supported at the moment as it will produce a circular dependency between chunks and will likely lead to broken execution order.

即:代码中的 modules 存在循环依赖,目前还不能很好的支持这种情况。

本以为内存溢出或许和这里有关系,在修改了800多个文件去除了所有的循环引用后测试,内存依然溢出。

虽然没有解决此问题,但是在开发时代码运行貌似有所提升,而且偶现的 Unexpected error when starting the router: ReferenceError: Cannot access '***' before initialization 也不再出现(相关:you can't eagerly access imported bindings of a file that's later in the import chain)。

排查在build的哪一阶段内存飙升

  • 我们可以通过rollup的钩子,逐个排查在哪一阶段内存开始飙升的。
  • 观测钩子log和内存监视器。定位内存消耗在哪一阶段。
  • 但有些时候钩子函数并不足够细致可以让我们排查问题,这时候把 rollup 源代码 clone 到本地使用 npm link,在本地源码调试。

暂时的解决方案

把配置中的 "build": "node --max_old_space_size=16384 ./node_modules/vite/bin/vite.js build --mode dev"max_old_space_size=16384 改为 8192 解决了 jenkinskill 进程。

据运维说是 jenkins 一共分配了 16g ,而运行这一个程序就请求 16g 空间,因此才会被kill。

这样改过之后依然偶有 build killed,还是要从官网建议的代码优化方面入手。之后有时间再尝试整理。