AI 微前端性能优化之旅(中):重启

背景

项目基于 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。项目早期引入的 tippyvxe-table 等 UI 库,后来大部分功能已经被替代,但依赖和旧代码还留在项目里。

这部分没有什么特别聪明的办法,就是一点点查找、替换和验证。工作很碎,但清理掉之后,能同时减少构建体积和后续维护成本。

路由和语言包改成按需加载

页面懒加载失效的原因很直接:Vite 读取路由文件时配置了 eager: true,所有路由模块都会在初始化阶段被加载。

去掉同步加载后,路由模块改为按需获取,对应页面代码和语言包也不用再一次性全部请求。

在这个前提下,语言包是否继续合并,差别已经没有之前那么大。它可以继续优化,但不再是主要瓶颈,所以我暂时没有把精力放在这里。

继续压首屏资源和请求数

对比本地和测试环境后,我发现静态资源的 gzip 压缩阈值被设得接近 1 MB,导致一些几百 KB 的文件没有被压缩。

调整到几十 KB 后,这部分文件的传输体积降到了原来的四成左右。考虑到很小的文件压缩和解压也有成本,我没有把阈值设得特别低,而是根据项目里的资源分布做了折中。特定环境下,首屏加载时间缩短了大约四分之一。

图标也是一个容易被忽略的点。项目原来的 Iconify 引入方式带进了几 MB 的 Lucide 图标资源,但实际使用的只是其中很少一部分。我把需要的图标改成本地按需引用,去掉了整套图标资源的加载。

请求数量过多时,浏览器排队造成的 Stalled 时间也会很明显。刚开始我尝试把单个依赖库的代码全部打进一个包,结果请求数是少了,单个包却变得太大。

后来改成只提取各项目都会用到的公共部分,在包体积和请求数量之间做平衡。代价还是一样:一旦项目使用了共享包里没有的新功能,就需要重新构建并更新共享包。

一个最后作废的方案

排查大文件时,我发现一个名为 installCanvasRenderer 的文件体积比较大。继续分析后确认,它其实是 ECharts 的 Canvas 渲染模块,被 Vite 自动拆成了独立文件。

我尝试把渲染方式改成 SVG,但构建体积和实际性能几乎都没有变化,所以这个方案最后直接作废。

这也算是这轮优化里比较典型的一次:文件大、名字显眼,不代表它就是瓶颈。最后还是得看改完后的数据。

总结

这一轮做下来,我的感觉是:AI 很容易盯着显眼的地方给方案,但显眼不等于真有问题。前面不少思路听着都能讲通,最后还是治标不治本。

真正有用的改动,基本都是先把加载链路捋清楚之后做的:页面按需加载、公共依赖复用、压缩配置、请求数量,还有页面切换的时序。

AI 不是没用,分析代码、整理 Network 记录和补实现都能省时间。但方向还是得自己定,性能优化最后也只认实测结果。不测的话,很容易忙半天,方案做得挺完整,页面却没快多少。

相关推荐
用户938515635071 小时前
Type vs Interface:读完这篇就没有面试官能难倒你了
前端·面试·typescript
油丶酸萝卜别吃2 小时前
jquery-ajax.js 说明文档
前端·javascript·jquery
windliang2 小时前
Claude Code 源码分析(九):子 Agent 如何分叉、继续与回到父会话
前端·javascript·面试
柒和远方3 小时前
V063: TS 面试必考:interface 与 type 的四大差异,与 LLM Harness 的自动化择优
前端·javascript
半个落月3 小时前
React useRef 详解:DOM 引用、持久化值与 Worker 实例
前端·react.js
阿黎梨梨3 小时前
TypeScript 类型编程:从新手到 Harness 工程实践
前端
黄金决明子3 小时前
Vue3 + Vite 打包后打开空白?
前端
张龙6873 小时前
终端效率翻倍实战:fzf + zoxide + ripgrep + bat 组合拳,告别重复敲命令
前端
宿6743 小时前
vue3-env环境
前端·vue.js
蔬菜_3 小时前
前端转全栈-day5(数组、list、set)
java·前端·数据结构·list