最近在服务器上打包我的博客后台管理系统,发现一个很现实的问题:项目本身不算特别大,但 vite build 很吃内存,内存小一点的机器容易卡住,甚至直接报 JavaScript heap out of memory。

这次我没有先把 Node 的内存上限调得更大,而是先看构建到底把什么东西装进了内存。最后主要处理了两个问题:Element Plus 图标被全量引入,ECharts 也被全量引入。

先说结论

构建时最怕的不是某个文件特别大,而是入口文件把一整套库都拉进来了。构建工具需要读取、分析、摇树优化和拆分这些模块,模块越多,构建阶段占用的内存就越高。

这次的处理方式很简单:

  • 只导入实际使用的图标。
  • 只注册实际使用的 ECharts 图表和组件。
  • 多个页面共用同一份 ECharts 配置,避免每个页面各自从完整入口导入。

优化后再次执行 npm run build 成功,实测 Node 构建进程峰值工作集约为 1.25 GB,没有触发 1.4 GB 的限制。

第一个问题:为什么一个图标导入会拖累构建

原来的写法是:

JavaScript
import * as Icons from "@element-plus/icons-vue";

app.config.globalProperties.$icon = Icons;

这段代码的意思是:把整个图标包都导入,然后运行时再通过字符串取图标:

Vue
<component :is="$icon[item.meta.icon]" />

这样写很方便,但对构建工具不太友好。因为 $icon 是一个动态对象,构建工具很难确定最终会用到哪些属性。最保险的做法就是把所有图标都保留下来。

但项目菜单实际用到的图标并没有那么多,比如:HomeFilledUserDataAnalysisSettingPicture 等。没有必要为了使用几十个图标,把整个图标集合都交给构建工具分析。

改成显式图标映射

现在改成了显式导入:

JavaScript
import {
  HomeFilled,
  User,
  DataAnalysis,
  Setting,
  Picture,
} from "@element-plus/icons-vue";

const menuIcons = {
  HomeFilled,
  User,
  DataAnalysis,
  Setting,
  Picture,
};

app.config.globalProperties.$icon = menuIcons;

菜单里的用法不用改,仍然可以按照名称取图标:

Vue
<component :is="$icon[item.meta.icon]" />

关键变化在于:现在这个对象里只放项目实际需要的图标。这样既保留了原来的动态菜单写法,又让打包工具知道哪些图标可以被裁掉。

第二个问题:不要直接把完整 ECharts 导入每个页面

项目中原来有很多这样的代码:

JavaScript
import * as echarts from "echarts";

ECharts 功能很多,除了柱状图、折线图、饼图,还有地图、漏斗图、雷达图、仪表盘、数据缩放、视觉映射等大量模块。

但当前项目实际只用到了几类图表。如果每个页面都从完整入口导入,构建工具就要处理完整 ECharts 入口,内存和产物体积都会受到影响。

建一个公共的 ECharts 入口

我新增了 src/utils/echarts.js,只注册当前项目使用的模块:

JavaScript
import * as echarts from "echarts/core";
import {
  BarChart,
  FunnelChart,
  LineChart,
  PieChart,
} from "echarts/charts";
import {
  DataZoomComponent,
  GridComponent,
  LegendComponent,
  TitleComponent,
  TooltipComponent,
} from "echarts/components";
import { CanvasRenderer } from "echarts/renderers";

echarts.use([
  BarChart,
  FunnelChart,
  LineChart,
  PieChart,
  DataZoomComponent,
  GridComponent,
  LegendComponent,
  TitleComponent,
  TooltipComponent,
  CanvasRenderer,
]);

export { echarts };

之后页面统一这样引用:

JavaScript
import { echarts } from "@/utils/echarts";

这样做有两个好处:

  1. 所有页面共用同一套 ECharts 模块,不会各自维护一套注册逻辑。
  2. 后面如果要增加图表类型,只需要在一个地方增加模块。

比如以后真的要用雷达图,只需要补上:

JavaScript
import { RadarChart } from "echarts/charts";

echarts.use([RadarChart]);

不用每个页面都重新改一遍。

为什么没有直接把所有页面合成一个大包

有些人遇到构建内存问题,会想到把所有代码合成一个文件,觉得文件少了构建就会更快。这个方向通常不对。

项目路由本来已经使用了懒加载:

JavaScript
component: () => import("@/views/Analytics/Index.vue")

这意味着用户访问哪个页面,浏览器再加载哪个页面的代码。强行合并成一个大包,可能让构建看起来文件少了,但会带来两个问题:

  • 首屏加载更多无关代码。
  • 用户访问一个简单页面时,也要下载图表、编辑器、Excel 等功能。

所以这里保留路由拆分,只处理不必要的全量依赖。

哪些方案看起来有效,但不建议先做

1. 只把 Node 内存上限调大

例如:

Shell
NODE_OPTIONS=--max-old-space-size=4096 vite build

这只能让构建在更大的机器上继续跑,不能解决依赖过多的问题。如果服务器只有 2 GB 或 4 GB 内存,调大上限反而可能把整台服务器拖死。

更合理的顺序是:先减少构建图,再根据机器情况设置一个合适的内存上限。

2. 一上来关闭代码压缩

关闭压缩有时会让某个阶段快一点,但产物会变大,最终用户下载的代码也会变多。而且当前项目已经使用了内存开销较低的 esbuild 压缩,没有必要为了省一点构建时间牺牲产物质量。

3. 直接删除所有构建插件

图片压缩、gzip、自动导入、组件自动注册等插件作用不同,不能看到构建慢就全部删掉。更稳妥的方式是先确认插件是否参与了当前构建,以及它是否真的消耗了主要内存。

当前项目的低内存构建模式已经关闭了 gzip 文件生成和图片压缩,这比无差别删除插件更安全。

4. 看到一个大文件就手动复制一份“精简版”库

这种做法短期可能有效,但容易造成项目维护问题。比如自己复制一份 ECharts 文件,后面升级依赖、修复漏洞、排查图表问题都会变麻烦。优先使用依赖本身提供的模块化入口,更容易维护。

这次改了哪些文件

核心改动有两类:

  • src/main.js:全量图标改为显式按需映射。
  • src/utils/echarts.js:新增 ECharts 模块化公共入口。
  • 各个图表页面:从 import * as echarts from "echarts" 改为引用公共入口。

没有修改业务接口、路由逻辑和页面交互,也没有动原来已经存在的 README.md 修改。

最后怎么验证

每次构建优化后,都至少执行:

Shell
npm run build

除了看命令是否成功,还要看三件事:

  • 有没有出现内存溢出。
  • 图表模块是否能正常初始化。
  • 路由懒加载页面是否仍然能生成对应的 chunk。

这次构建最终成功,ECharts 生成了约 552 KB 的独立 chunk,图表模块也能正常初始化。构建过程中仍有组件命名冲突和 Sass 弃用提示,但这些是原有警告,不是本次优化引入的问题。

还可以继续怎么做

如果后面服务器内存仍然紧张,我会继续按这个顺序排查:

  1. 检查 md-editor-v3 是否加载了不需要的语言包和编辑器功能。
  2. 检查 exceljs 是否只在真正需要导入导出的页面加载。
  3. 检查自动导入和组件扫描范围,避免扫描不必要的目录。
  4. 记录每次构建的峰值内存,不在只凭感觉改配置。

原则还是一样:先找出被加载的东西,再决定删什么、拆什么、延迟什么。单纯把内存上限调大,通常只是把问题往后推。

阅读进度 0%