最近在服务器上打包我的博客后台管理系统,发现一个很现实的问题:项目本身不算特别大,但 vite build 很吃内存,内存小一点的机器容易卡住,甚至直接报 JavaScript heap out of memory。
这次我没有先把 Node 的内存上限调得更大,而是先看构建到底把什么东西装进了内存。最后主要处理了两个问题:Element Plus 图标被全量引入,ECharts 也被全量引入。
先说结论
构建时最怕的不是某个文件特别大,而是入口文件把一整套库都拉进来了。构建工具需要读取、分析、摇树优化和拆分这些模块,模块越多,构建阶段占用的内存就越高。
这次的处理方式很简单:
- 只导入实际使用的图标。
- 只注册实际使用的 ECharts 图表和组件。
- 多个页面共用同一份 ECharts 配置,避免每个页面各自从完整入口导入。
优化后再次执行 npm run build 成功,实测 Node 构建进程峰值工作集约为 1.25 GB,没有触发 1.4 GB 的限制。
第一个问题:为什么一个图标导入会拖累构建
原来的写法是:
import * as Icons from "@element-plus/icons-vue";
app.config.globalProperties.$icon = Icons;
这段代码的意思是:把整个图标包都导入,然后运行时再通过字符串取图标:
<component :is="$icon[item.meta.icon]" />
这样写很方便,但对构建工具不太友好。因为 $icon 是一个动态对象,构建工具很难确定最终会用到哪些属性。最保险的做法就是把所有图标都保留下来。
但项目菜单实际用到的图标并没有那么多,比如:HomeFilled、User、DataAnalysis、Setting、Picture 等。没有必要为了使用几十个图标,把整个图标集合都交给构建工具分析。
改成显式图标映射
现在改成了显式导入:
import {
HomeFilled,
User,
DataAnalysis,
Setting,
Picture,
} from "@element-plus/icons-vue";
const menuIcons = {
HomeFilled,
User,
DataAnalysis,
Setting,
Picture,
};
app.config.globalProperties.$icon = menuIcons;
菜单里的用法不用改,仍然可以按照名称取图标:
<component :is="$icon[item.meta.icon]" />
关键变化在于:现在这个对象里只放项目实际需要的图标。这样既保留了原来的动态菜单写法,又让打包工具知道哪些图标可以被裁掉。
第二个问题:不要直接把完整 ECharts 导入每个页面
项目中原来有很多这样的代码:
import * as echarts from "echarts";
ECharts 功能很多,除了柱状图、折线图、饼图,还有地图、漏斗图、雷达图、仪表盘、数据缩放、视觉映射等大量模块。
但当前项目实际只用到了几类图表。如果每个页面都从完整入口导入,构建工具就要处理完整 ECharts 入口,内存和产物体积都会受到影响。
建一个公共的 ECharts 入口
我新增了 src/utils/echarts.js,只注册当前项目使用的模块:
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 };
之后页面统一这样引用:
import { echarts } from "@/utils/echarts";
这样做有两个好处:
- 所有页面共用同一套 ECharts 模块,不会各自维护一套注册逻辑。
- 后面如果要增加图表类型,只需要在一个地方增加模块。
比如以后真的要用雷达图,只需要补上:
import { RadarChart } from "echarts/charts";
echarts.use([RadarChart]);
不用每个页面都重新改一遍。
为什么没有直接把所有页面合成一个大包
有些人遇到构建内存问题,会想到把所有代码合成一个文件,觉得文件少了构建就会更快。这个方向通常不对。
项目路由本来已经使用了懒加载:
component: () => import("@/views/Analytics/Index.vue")
这意味着用户访问哪个页面,浏览器再加载哪个页面的代码。强行合并成一个大包,可能让构建看起来文件少了,但会带来两个问题:
- 首屏加载更多无关代码。
- 用户访问一个简单页面时,也要下载图表、编辑器、Excel 等功能。
所以这里保留路由拆分,只处理不必要的全量依赖。
哪些方案看起来有效,但不建议先做
1. 只把 Node 内存上限调大
例如:
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 修改。
最后怎么验证
每次构建优化后,都至少执行:
npm run build
除了看命令是否成功,还要看三件事:
- 有没有出现内存溢出。
- 图表模块是否能正常初始化。
- 路由懒加载页面是否仍然能生成对应的 chunk。
这次构建最终成功,ECharts 生成了约 552 KB 的独立 chunk,图表模块也能正常初始化。构建过程中仍有组件命名冲突和 Sass 弃用提示,但这些是原有警告,不是本次优化引入的问题。
还可以继续怎么做
如果后面服务器内存仍然紧张,我会继续按这个顺序排查:
- 检查
md-editor-v3是否加载了不需要的语言包和编辑器功能。 - 检查
exceljs是否只在真正需要导入导出的页面加载。 - 检查自动导入和组件扫描范围,避免扫描不必要的目录。
- 记录每次构建的峰值内存,不在只凭感觉改配置。
原则还是一样:先找出被加载的东西,再决定删什么、拆什么、延迟什么。单纯把内存上限调大,通常只是把问题往后推。
private note
交流
文章暂不开放公开评论。如果你有想法、问题或建议,欢迎私下联系站长。
联系站长 →