背景
项目基于 Vue 和 wujie 实现,这一轮主要使用的模型是 GPT-5.4 和 mimo-2.5-pro。
上一篇里,我直接让 AI 分析代码和浏览器的 Network 记录,拿到了一批看起来很完整的优化建议。但真正落地后,效果并不明显,有些改动甚至还增加了额外成本。
所以这一轮我换了个方式:先自己把加载链路和页面切换过程重新梳理一遍,再让 AI 帮我分析代码、补实现和验证思路。
先把问题重新捋一遍
手工排查后,主要发现了这些问题:
- 首次加载资源超过 10 MB,请求数达到数百个。在带宽有限的环境里,页面加载接近 10 秒,单看资源体积和请求数,其实已经能解释大部分耗时。
- 为了实现页面保活,之前的方案给每个页面单独创建了一个 wujie 实例。不仅可能造成卡顿,每打开一个新页面,还会重新请求一遍子应用资源。
- 为了让子应用可以单独加载,菜单配置通过 Vite 引入本地路由模块,但路由模块使用了同步加载,结果所有页面代码都在初始化时被带了进来。
- 语言包没有合并,再叠加路由同步加载,初始化阶段会一次请求几十个语言包文件。
- 主、子应用的公共依赖没有真正复用,同一套库会被重复下载、解析和执行。
- 第一次点击新菜单时,会先出现 loading,接着旧页面闪一下,最后才显示新页面。原因是异步加载和路由重定向之间存在短暂间隙,这时旧页面的 DOM 还没有被移除。
这次列完问题后,方向就清楚多了。真正需要处理的不是某一个显眼的大文件,而是资源体积、请求数量、重复加载和页面切换时序这几件事。
公共依赖复用
要复用公共依赖,第一步是统一主、子应用的依赖版本,后面才有讨论方案的基础。
我一开始尝试了 external + CDN,但依赖之间的引用关系很容易引发运行时报错。之后又试了 manualChunks,即使做了比较细的分包,不同项目受自身引用链影响,最终产物仍然不完全一致,也很难直接复用。
最后我选择用 import map 处理。
因为部分公共资源服务在当前网络环境下不够稳定,我把公共库和少量小依赖打成 ESM 包,部署到可控的静态资源服务上,再通过 import map 统一映射。
比如引入 Pinia 时,可以把 Pinia 和它的一些小依赖打到一起,同时把体积更大的 Vue 排除出去。运行时再由 import map 负责把 Pinia 依赖的 Vue 指向统一版本。
这个方案能减少重复加载,但也带来了维护成本:如果某个项目开始使用共享包里没有包含的新功能,就需要重新构建共享包,并同步到使用它的项目中。
页面切换时,旧页面会先闪一下
这个问题让 AI 调了几轮,最后的处理方式是:路由开始切换时,先把旧组件的 DOM 主动摘掉,等新组件准备好后再恢复渲染。
这里有个比较麻烦的点:DOM 可以暂时不展示,但组件实例需要保留,否则页面保活会失效。连续切换菜单时,还要取消上一轮等待展示的逻辑,避免旧任务回来抢状态。
最终使用 KeepAlive + v-if 控制:
vue
<KeepAlive :exclude="getExcludeCacheTabs" :include="keepAliveInclude">
<component
:is="realComponent"
v-if="showComponent(route)"
:key="fullPath"
/>
</KeepAlive>
这里没有用 v-show,是因为旧页面的 DOM 即使被隐藏,仍可能影响新页面的渲染和切换过程。
路由模块会在 beforeEach 中按需加载并完成注册,然后重新重定向一次。原因是第一次路由匹配发生时,动态路由还没有注册,不重新匹配就会进入 404。
新页面的展示时机则放到 afterEach 之后,再通过 requestAnimationFrame 等待旧页面的 DOM 的移除来完成更新。
另外,如果项目里有页面切换动画,也需要一起检查。动画生效期间,旧页面的 DOM 可能一直存在,前面的处理就会被抵消。这个点是我把主要问题处理完后才想到的,前面问了 AI 很多轮,它也一直没有提到。用下来感觉,AI 能解决眼前的代码,但对完整交互链路的覆盖还是不太够。
页面保活
之前为了实现保活,页面和 wujie 实例绑得太紧,反而带来了重复加载和状态管理问题。
这一轮改回使用 wujie 自带的 alive 配置。中间也让 AI 返工了几轮,不过这个能力本身是开箱即用的,最终还是回到了框架推荐的正常写法。
这部分我主要验证了最终效果,没有过多保留中间每一次改动。对这种框架已经提供标准能力的问题,能用正常配置解决,就没必要再额外造一层逻辑。
清理重复依赖
整个微前端项目主要使用 antdv-next。项目早期引入的 tippy、vxe-table 等 UI 库,后来大部分功能已经被替代,但依赖和旧代码还留在项目里。
这部分没有什么特别聪明的办法,就是一点点查找、替换和验证。工作很碎,但清理掉之后,能同时减少构建体积和后续维护成本。
路由和语言包改成按需加载
页面懒加载失效的原因很直接:Vite 读取路由文件时配置了 eager: true,所有路由模块都会在初始化阶段被加载。
去掉同步加载后,路由模块改为按需获取,对应页面代码和语言包也不用再一次性全部请求。
在这个前提下,语言包是否继续合并,差别已经没有之前那么大。它可以继续优化,但不再是主要瓶颈,所以我暂时没有把精力放在这里。
继续压首屏资源和请求数
对比本地和测试环境后,我发现静态资源的 gzip 压缩阈值被设得接近 1 MB,导致一些几百 KB 的文件没有被压缩。
调整到几十 KB 后,这部分文件的传输体积降到了原来的四成左右。考虑到很小的文件压缩和解压也有成本,我没有把阈值设得特别低,而是根据项目里的资源分布做了折中。特定环境下,首屏加载时间缩短了大约四分之一。
图标也是一个容易被忽略的点。项目原来的 Iconify 引入方式带进了几 MB 的 Lucide 图标资源,但实际使用的只是其中很少一部分。我把需要的图标改成本地按需引用,去掉了整套图标资源的加载。
请求数量过多时,浏览器排队造成的 Stalled 时间也会很明显。刚开始我尝试把单个依赖库的代码全部打进一个包,结果请求数是少了,单个包却变得太大。
后来改成只提取各项目都会用到的公共部分,在包体积和请求数量之间做平衡。代价还是一样:一旦项目使用了共享包里没有的新功能,就需要重新构建并更新共享包。
一个最后作废的方案
排查大文件时,我发现一个名为 installCanvasRenderer 的文件体积比较大。继续分析后确认,它其实是 ECharts 的 Canvas 渲染模块,被 Vite 自动拆成了独立文件。
我尝试把渲染方式改成 SVG,但构建体积和实际性能几乎都没有变化,所以这个方案最后直接作废。
这也算是这轮优化里比较典型的一次:文件大、名字显眼,不代表它就是瓶颈。最后还是得看改完后的数据。
总结
这一轮做下来,我的感觉是:AI 很容易盯着显眼的地方给方案,但显眼不等于真有问题。前面不少思路听着都能讲通,最后还是治标不治本。
真正有用的改动,基本都是先把加载链路捋清楚之后做的:页面按需加载、公共依赖复用、压缩配置、请求数量,还有页面切换的时序。
AI 不是没用,分析代码、整理 Network 记录和补实现都能省时间。但方向还是得自己定,性能优化最后也只认实测结果。不测的话,很容易忙半天,方案做得挺完整,页面却没快多少。