# 前端打包优化说明(build-mem-v1) > **一句话**:把生产构建从「被内核 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** | --- ## 1. 改动清单 | 文件 | 改动 | 作用 | | --- | --- | --- | | `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)。 --- ## 2. 环境前提 | 项 | 要求 / 现状 | | --- | --- | | 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\\.wslconfig` → `[wsl2] memory=12GB` / `swap=8GB`) | | 构建可用内存 | 需要 **约 9.7 GB**;若同时开着 VS Code(语言服务会到 3~5 GB),建议构建前关掉 Remote-WSL 窗口 | 国内网络可在本地 `.npmrc`(已被 gitignore)指镜像: ```ini registry=https://registry.npmmirror.com ``` --- ## 3. 原来的构建为什么会失败 原始配置 `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 之间。 --- ## 4. 内存到底花在哪(带埋点实测) 用 `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 | **三个关键结论:** 1. **96% 的堆在 `old_space`(普通 JS 对象)**,大字符串(`large_object`)只有 0.4~0.6 GB —— 不是图片、不是大文件,而是成千上万个 AST 节点对象。 2. **内存几乎在 transform 阶段就定型**,渲染/压缩/写产物基本不再增长。 3. **源码 106.88 MB → 堆 5.38 GB ≈ 50 倍放大**,平均约 **420 KB/模块**。 **决定内存的是模块数量,不是文件大小** —— 这就是「产物 JS 只有 63 MB,构建却要 5 GB+」的原因。 > Rollup 3 为每个模块保留:源码字符串 + ESTree AST(纯 JS 对象,最占内存)+ 作用域/绑定信息 + 渲染产物。 --- ## 5. 三项优化 ### 5.1 依赖去重(`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 | ```json "pnpm": { "overrides": { "echarts": "6.1.0", "xlsx": "0.18.5", "three": "0.162.0" } } ``` 验证:`ls node_modules/.pnpm | grep -E '^(echarts|three|xlsx)@'` 每项只出现一次。 ### 5.2 移除 `@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` 注释里。 ### 5.3 gzip 压缩级别:保持默认 9(不改) 曾试过降到 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。 ### 5.4 堆上限 12288 → 8192 见 §3 的实测:6144 / 7168 都会 V8 堆溢出,8192 是能跑完的最小值。 --- ## 6. 收益汇总(实测) ### 6.1 分阶段演进(埋点数据) | 指标 | 原始 | + 依赖去重 | + 移除 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** | ### 6.2 A/B 真实构建(同一插件、同一依赖,唯一变量 = drag-free) | 指标 | 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** | --- ## 7. 图片压缩:为什么保持原方案 中途曾把图片编码器换成 `sharp`(理由是它任何平台零编译、且压缩率略好),并把压缩移出 Vite 进程。 **最终已全部回退**,原因: - 图片压缩**与内存瓶颈无关**:sharp 峰值仅约 375 MB,而构建瓶颈是 Rollup 的模块图(§4); - 原方案在内存问题解决后完全跑得动,没必要引入新依赖。 当前状态:`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 压不动),产物中保持原样 —— 这是原版行为。 --- ## 8. 验证记录 | 项 | 结果 | | --- | --- | | 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 / 包围盒与输入一致 / 无 NaN - 多段折线带圆角、三维斜线 → ✅ - 它只用到 10 个 three 基础 API(`BufferAttribute`、`BufferGeometry`、`DynamicDrawUsage`、 `StaticDrawUsage`、`Matrix4`、`QuadraticBezierCurve3`、`Uint16/32BufferAttribute`、`Vector3`), r162 全部存在且签名未变 **两个注意点:** 1. `three.path` 的 **CJS/UMD 构建(`main` 字段)在现代 three 上会崩** (`Class constructor BufferGeometry cannot be invoked without 'new'`,因为它是 ES5 转译的)。 Vite 走 `module` 字段用 ESM 构建,所以打包没问题;但 Jest / Node 脚本 `require` 它会挂。 2. 它内部用了 `BufferAttribute.updateRange()`,**r169 会被移除** —— 将来升 three 到 r169+ 需一并处理。 3. npm 上 `three.path` 只有 1.0.0 / 1.0.1(均 2023-02),**没有可升级版本**。 --- ## 9. 已知风险与后续可做 | 优先级 | 事项 | 预期收益 | | --- | --- | --- | | 中 | 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` 等) 会成倍放大,而对构建内存的进一步帮助有限。 --- ## 10. 附录:常用命令 ```bash # 常规构建 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 的还原注释。