一句话:把生产构建从「被内核 OOM Killer 杀掉、根本跑不完」变成「稳定跑完」, 并让单次构建省约 3 分钟、峰值内存降 1.1 GB、JS 产物降 10 MB。
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 构建结果 | ❌ 7:43 被内核杀掉 | ✅ 18:34 完成 |
| 构建峰值 RSS | 8.49 GB(被杀时) | 9.73 GB(含 swap 兜底) |
| 参与打包的模块数 | 13,098 | 11,913 |
| JS 产物 | 63 MB | 53 MB |
| dist 总大小 | 296 MB | 288 MB |
| 文件 | 改动 | 作用 |
|---|---|---|
package.json |
堆上限 12288 → 8192 | V8 不再越过物理内存失控 |
package.json |
新增 pnpm.overrides(echarts / xlsx / three) |
消除三套重复依赖 |
package.json |
新增 packageManager: pnpm@9.15.9 |
防止用 pnpm 8 重解析依赖 |
package.json |
新增 build:lean |
跳过 gzip 的快速构建 |
src/components/registerGlobComp.ts |
移除 @qiaoqiaoyun/drag-free |
不再把整包 antd + 421 个图标拖进构建 |
.gitignore |
忽略 .pnpm-store |
防止 pnpm 本地仓库进版本库 |
docs/build-memory.md |
本文档 | — |
图片压缩保持原方案 vite-plugin-imagemin 不变(原因见 §7);gzip 保持默认最高级别 9(见 §5.3)。
| 项 | 要求 / 现状 |
|---|---|
| Node | ≥ 18.12(本项目实际 v22.21.0) |
| pnpm | 必须 9.x。仓库 pnpm-lock.yaml 是 lockfileVersion: '9.0',用 pnpm 8 会重解析依赖并把 vue 降到 3.2.0,构建直接报 Failed to resolve vue/compiler-sfc |
| WSL2 内存 | 12 GB + swap 8 GB(C:\Users\<user>\.wslconfig → [wsl2] memory=12GB / swap=8GB) |
| 构建可用内存 | 需要 约 9.7 GB;若同时开着 VS Code(语言服务会到 3~5 GB),建议构建前关掉 Remote-WSL 窗口 |
国内网络可在本地 .npmrc(已被 gitignore)指镜像:
registry=https://registry.npmmirror.com
原始配置 NODE_OPTIONS=--max-old-space-size=12288(12 GB)高于机器物理内存。
V8 按上限规划回收节奏,于是「不急着回收」,实测:
| 指标 | 结果 |
|---|---|
| 耗时 | 7 分 43 秒 |
| 进程峰值 RSS | 8.49 GB |
| 结局 | 被内核 OOM Killer 杀掉(anon-rss:8488300kB、total-vm:42239712kB) |
| 产物 | dist 只写了一半(260 / 4463 文件) |
堆上限压低后也证明这条路走不通:6144 直接 V8 堆溢出,7168 在 21 分钟时堆溢出,只有 8192 能跑完。 说明构建的真实需求就在 7~8 GB 之间。
用 v8.getHeapSpaceStatistics() 在关键阶段打点:
| 时刻 | 阶段 | heapUsed | old_space | large_object | RSS |
|---|---|---|---|---|---|
| t+0s | buildStart | 0.07 GB | 41 MB | 16 MB | 0.17 GB |
| t+168s | transform 2500 模块 | 4.30 GB | — | — | 5.29 GB |
| t+354s | transform 完成(13,098 模块) | 5.38 GB | — | — | 6.03 GB |
| t+356s | renderStart | 5.38 GB | 5,179 MB | 404 MB | 6.03 GB |
| t+461s | generateBundle | 5.47 GB | 5,598 MB | 635 MB | 6.73 GB |
| t+659s | writeBundle | 5.35 GB | — | — | 6.24 GB |
三个关键结论:
old_space(普通 JS 对象),大字符串(large_object)只有 0.4~0.6 GB
—— 不是图片、不是大文件,而是成千上万个 AST 节点对象。Rollup 3 为每个模块保留:源码字符串 + ESTree AST(纯 JS 对象,最占内存)+ 作用域/绑定信息 + 渲染产物。
pnpm.overrides)三个间接依赖的版本范围与项目主版本没有交集,pnpm 只能同时装两份,两份都会被打进构建:
| 重复库 | 项目用 | 谁拖进来的旧版 | 旧版范围 |
|---|---|---|---|
| echarts | 6.1.0 | @qiaoqiaoyun/drag-free |
^5.4.3 → 5.6.0(还带 echarts-gl / echarts-liquidfill) |
| xlsx | 0.18.5 | @qiaoqiaoyun/drag-free |
^0.17.0 → 0.17.5 |
| three | 0.162.0 | three.path |
^0.132.2 → 0.132.2 |
"pnpm": {
"overrides": {
"echarts": "6.1.0",
"xlsx": "0.18.5",
"three": "0.162.0"
}
}
验证:ls node_modules/.pnpm | grep -E '^(echarts|three|xlsx)@' 每项只出现一次。
@qiaoqiaoyun/drag-free它的 UMD(1.4 MB)用 require('ant-design-vue') / require('@ant-design/icons-vue')
把整包拖进构建 —— CJS require 无法 tree-shake。
| 证据 | 移除前 | 移除后 |
|---|---|---|
antd-vue-vendor-*.js |
2.06 MB | 1.09 MB |
打包进去的 *Outlined 图标数 |
421(整套) | 82(实际用到的) |
从未被使用的 AccountBookOutlined |
在产物里 | 消失 |
该功能("敲敲云—仪表盘设计器")在本项目没有任何路由或代码引用,src/views/super/drag/
9 个文件是孤立页面。还原方式已写在 registerGlobComp.ts 注释里。
曾试过降到 6(体积 +0.26%、耗时 -28%),但本项目更在意产物体积,已回退为 zlib 的默认最高级别 9。
level 9 就是 gzip 的上限,再调没有空间了:
| 变体 | 体积变化 | 说明 |
|---|---|---|
level 9(现用) |
基准 | gzip 最高压缩比 |
level 9 + memLevel=9 |
+0.22%(更大) | 无效 |
level 9 + strategy=FIXED |
+22.9%(更大很多) | 无效 |
level 6 |
+0.26% | 更快但更大,已弃用 |
若还想更小,只有换 brotli(40 个大文件实测):
| 方案 | 体积 | 相对 gzip-9 | 耗时 |
|---|---|---|---|
| gzip -9(现用) | 8.79 MB | 基准 | 0.72 s |
| brotli q5 | 8.16 MB | -7.2% | 0.43 s(比 gzip 更快) |
| brotli q9 | 7.96 MB | -9.5% | 1.65 s |
| brotli q11(插件默认) | 7.47 MB | -15.1% | 30.1 s(41×,会拖慢构建 1~1.5 分钟) |
启用方式:.env.production 里 VITE_BUILD_COMPRESS = 'gzip,brotli'(两种产物都出)。
前提是部署侧 nginx 支持 brotli 模块(brotli_static on),否则 .br 文件无人消费;
插件 brotli 分支默认用 BROTLI_MAX_QUALITY(11),建议顺手降到 5~9。
见 §3 的实测:6144 / 7168 都会 V8 堆溢出,8192 是能跑完的最小值。
| 指标 | 原始 | + 依赖去重 | + 移除 drag-free |
|---|---|---|---|
| 模块数 | 13,098 | 12,542 | 11,913 |
| 转换后源码总量 | 106.88 MB | 101.35 MB | 92.37 MB |
| buildEnd 堆 | 5.38 GB | 4.56 GB | 4.02 GB |
| buildEnd RSS | 6.03 GB | 5.82 GB | 4.69 GB |
| 总 JS 产物 | 63 MB | — | 53 MB |
| 指标 | A:保留 drag-free | B:移除 drag-free | 变化 |
|---|---|---|---|
| 模块数 | 12,541 | 11,913 | -628(-5.0%) |
| vite 阶段耗时 | 19m 45s | 17m 13s | -2m 32s(-12.8%) |
| 总耗时 | 21:16 | 18:34 | -2m 42s(-12.7%) |
| 峰值 RSS | 10.88 GB | 9.73 GB | -1.11 GB(-10.6%) |
| swap 峰值 | 0.79 GiB | 0.40 GiB | -49% |
| dist | 295 MB | 288 MB | -7 MB |
| 总 JS | 60 MB | 53 MB | -7 MB(-11.7%) |
antd-vue-vendor |
2.06 MB | 1.09 MB | -47% |
| 打包的图标数 | 421 | 82 | -339 |
中途曾把图片编码器换成 sharp(理由是它任何平台零编译、且压缩率略好),并把压缩移出 Vite 进程。
最终已全部回退,原因:
当前状态:build/vite/plugin/imagemin.ts 为原版 vite-plugin-imagemin@0.6.1,
在 generateBundle 内压缩,无缓存、无并发上限(原版行为)。
vite-plugin-imagemin 依赖 5 个原生工具,不是 npm 预编译包:
| 工具 | 来源 | 本机补齐方式 |
|---|---|---|
| gifsicle 1.92 | 包内 vendor/source/*.tar.gz |
./configure --without-simd && make |
| jpegtran (libjpeg-turbo 1.5.1) | 同上 | 同上 |
| cjpeg (mozjpeg 3.2) | 同上 | 同上 |
| pngquant 2.12.0 | 同上 | 缺 libpng-dev 且无 sudo → apt-get download libpng-dev zlib1g-dev 解包到本地,用 libpng-config 垫片喂给 configure |
| optipng 0.7.7 | 同上 | 自带 configure(不接受 --libdir,需单独处理) |
影响:全新 Linux 机器 pnpm install 可能因缺 gcc / make / autoconf / nasm / libpng-dev 而失败;
Windows 基本装不上。若这成为问题,可改回 sharp 方案(纯下载、零编译,压缩率相当)。
已知行为:72 MB 的 src/assets/images/gif/fanlocal.gif 每次构建都会打一条
imagemin error(gifsicle 压不动),产物中保持原样 —— 这是原版行为。
| 项 | 结果 |
|---|---|
| 5 个原生二进制可执行 | ✅ pngquant 2.12.0 / optipng 0.7.7 / gifsicle 1.92 / jpegtran 1.5.1 / cjpeg 3.2 |
| PNG 压缩 | ✅ 972 KB → 157 KB |
| JPEG 压缩 | ✅ 16 KB → 5 KB |
完整 pnpm build |
✅ built in 17m 13s,峰值 RSS 9.73 GB,swap 0.40 GiB |
| 最终产物 | 288 MB / 4494 文件 / JS 53 MB / 图片 84.1 MB |
three.path 与 three 0.162 兼容性 |
✅ 13 项几何用例全通过(见下) |
three.path 兼容性专项项目在 5 个三维风路文件里用 PathPointList / PathGeometry。因 pnpm.overrides 把 three 从
0.132.2 统一到 0.162.0,做了实跑验证(Node 里真实构造几何,不需要 WebGL):
set(points,0,0,up,false) + new PathGeometry(n,false) + update(...))
→ ✅ 顶点 4 / 索引 6 / 包围盒与输入一致 / 无 NaNBufferAttribute、BufferGeometry、DynamicDrawUsage、
StaticDrawUsage、Matrix4、QuadraticBezierCurve3、Uint16/32BufferAttribute、Vector3),
r162 全部存在且签名未变两个注意点:
three.path 的 CJS/UMD 构建(main 字段)在现代 three 上会崩
(Class constructor BufferGeometry cannot be invoked without 'new',因为它是 ES5 转译的)。
Vite 走 module 字段用 ESM 构建,所以打包没问题;但 Jest / Node 脚本 require 它会挂。BufferAttribute.updateRange(),r169 会被移除 —— 将来升 three 到 r169+ 需一并处理。three.path 只有 1.0.0 / 1.0.1(均 2023-02),没有可升级版本。| 优先级 | 事项 | 预期收益 |
|---|---|---|
| 中 | 8 个内联巨型 SVG 组件(zhijiangSVG 1.30 MB、fanTunnelDualSVG 1.03 MB 等,合计 7.2 MB)外置到 public/ 或改动态加载 |
少 8 个巨型 AST,估 -300~500 MB |
| 中 | 升级 Vite 5 + Rollup 4(原生 Rust 解析器,降低每模块开销) | 峰值或降至 6~6.5 GB,渲染快 1.5~2.5× |
| 低 | docs 里记录的 sharp 方案(若安装编译成为痛点可切回) |
安装零编译 |
| — | 需要人工冒烟的路径 | 4 个风机监控页(three.path 几何渲染效果)、主题浅色/深色切换 |
不要一步跳到 Vite 7:迁移面(unocss、vite-plugin-purge-icons、@rys-fe/vite-plugin-theme 等)
会成倍放大,而对构建内存的进一步帮助有限。
# 常规构建
pnpm build
# 跳过 gzip 的快速构建
pnpm build:lean
# 必须用 pnpm 9(锁文件是 v9 格式)
npx pnpm@9 install
corepack enable pnpm && pnpm install # 已写 packageManager,会锁定 9.15.9
# 校验依赖树是否一致(应输出 "Lockfile is up to date")
npx pnpm@9 install --frozen-lockfile
# 查看去重是否生效
npx pnpm@9 why echarts
ls node_modules/.pnpm | grep -E '^(echarts|three|xlsx)@'
# 清缓存(含图片缓存目录,若日后接入)
pnpm clean:cache
回滚点:本次改动都是小步可逆的 ——
堆上限/gzip 级别是单行数值;pnpm.overrides 删掉即可;
registerGlobComp.ts 里保留了 drag-free 的还原注释。