最近遇到一个性能问题,我让大模型自己去处理,然后就去午休了,醒来之后,它还真的把问题找出来并修复了
先说结果
最终定位出来的根因是:
这个业务工作台的查询表单,把 Formily 的
form实例塞进了带 devtools 的 Zustand store。
这件事在开发环境里会非常要命,因为 form 不是一个轻量对象,它里面带着大量字段状态、reaction、effect、schema 和联动信息。
一旦进了 store,又被 devtools 观察、快照、序列化,内存就会被瞬间放大。
如果把这里面的角色先拆开,其实更容易理解:
- Formily form 是一个重量级运行时实例,里面不只是表单值,还带着字段树、联动关系和各种内部状态
- Zustand store 适合放跨组件共享的轻量状态,比如布尔值、筛选条件、选中 id 这类数据
- devtools 在开发环境下会持续记录 store 变化,所以一旦把重对象塞进去,调试链路里的成本也会跟着被放大
更准确地说,这里的问题不是"Zustand 绝对不能放 Formily 对象",而是:
Formily form 这种重量级运行时实例,本来就不适合进会被 devtools 观察、快照和序列化的 store state;而 devtools 又把这个问题成倍放大了。
所以如果只问"主要是不是 devtools 的影响",答案是:
- 是,主要是 devtools 把问题放大了
- 但根上还是对象放错了位置
- 就算没有 devtools,这种设计也依然不优雅,只是未必会炸得这么明显
修完之后,Playwright 复现出来的内存数据从:
- 修复前:839MB ~ 887MB
变成了:
- 修复后:165MB ~ 207MB
这里的数字来自页面里已有的内存日志采样,主要用来做同一场景下的前后对比,不是一份精确的 heap profile。
也就是说,那个最扎眼的"900MB 级内存高峰",确实被打下来了。
问题是什么?
我做的是一个后台工作台页面,页面主体可以简单理解成三块:
- 一块复杂的查询表单
- 一块结果表格
- 一块会反向改写查询条件的数据报表
也就是说,这不是一个"填完表单点搜索就结束"的页面,而是多个区域都会读写同一套查询状态。这也是为什么问题后来会收敛到 query form:当不同模块都想访问同一个表单实例时,最容易出现"顺手把 form 放进全局 store"这种设计。
最开始看到的问题很直接:
- 页面一打开,内存就冲到 900MB 左右
- 这个高内存不是闪一下就没了,而是会持续一段时间
- 差不多要到 30 秒左右 才明显掉下来
- 一热更新,页面还很容易直接崩掉
- 切到别的页面时,也经常会被一起拖崩
这里先补一句:30 秒后会回落 这个现象确实存在,但这次排查的重点不是解释"它为什么回落",而是先找出"为什么峰值会被顶到这么离谱"。因为哪怕后面会掉下来,开发态里先冲到 900MB 也已经足够把热更新和页面稳定性一起拖垮了。
用户自己已经先排过一轮,确认问题大概率和查询表单有关,所以这次不是从整页瞎猜,而是直接围着 query form 往下查。
我是怎么给大模型下指令的
这个业务工作台页面打开以后,内存一直很高,差不多 900MB,要到第 30 秒左右才掉下来。
我已经查到是查询表单导致的,但具体原因还没找到。
你直接用 Playwright 打开页面看看。
我给它的信息其实很朴素:页面是一个业务工作台页面,现象是内存大约 900MB、30 秒后才回落,已知范围是查询表单,证据入口是 bootstrap.tsx 里那段内存日志,要求它必须直接用 Playwright 打开页面去看,而不是只给我猜测。
这里的 bootstrap.tsx 不是一个神秘线索,它只是项目启动入口里原本就有一段内存日志,所以模型一上来就能先接住现成证据,而不是从零开始搭监控。
这类输入的核心不是"怎么把提示词写得像魔法",而是:
把问题描述成一个可以执行、可以验证、可以收敛的任务。
大模型做了什么
它做的事情其实很像一个靠谱的前端同学在接手问题。
先用 Playwright 直接打开页面,不是先猜。它先拿 /index 做基线,确认首页内存大约在 110MB ~ 127MB ,说明不是整个站点天然就高;再去打开这个业务工作台页面,很快就把异常复现出来了:内存一路冲到 839MB ~ 887MB ,并且一直到 30 秒左右 才掉到 79MB 左右。到这一步,问题就从"感觉不对"变成了"机器也能稳定看到"。
这里的数字口径都来自同一套页面内存日志采样,重点是看同一页面、同一种采样方式下的前后差异,不是拿它去做跨页面的精确内存对账。
接着它没有在整页里乱翻,而是顺着"查询表单"继续往下查,很快就把链路串起来了:
- 查询表单组件先创建了
Formily form - 为了让别的模块也能访问它,代码又把这个
form实例写进了查询表单对应的 Zustand store - 这个 store 在开发环境下又接了
zustand/middleware/devtools
如果把这条链路翻译成人话,就是:
- 组件内部创建了一个很重的
form运行时对象 - 为了跨模块共享,代码把它塞进了 store state
- store 又接了 devtools,所以这个重对象进入了调试链路
- devtools 在当前开发环境下会持续记录和处理这些 state 变化
- 于是问题就不再只是"内存里有一个重对象",而是"这个重对象还被反复观察和放大"
根因也就很清楚了:Formily form 这种重量级运行时实例,本来就不适合放进会被 devtools 观察、快照和序列化的 store state;而 devtools 又把这个问题放大成了 900MB 级高峰。 所以这事不是"表单字段太多",而是"重对象被放错了位置,而且被 devtools 放大了"。
确定根因之后,它没有停在"给建议",而是直接把代码改了:修复前,查询表单把 Formily form 直接塞进 Zustand store state;修复后,store state 里只保留轻量的 hasForm,真正的实例改成闭包保存,通过 getForm() 按需读取,再把依赖 state.form 的地方一起改掉。
这里要注意,闭包保存实例 不是说这个对象突然就不存在了,而是说它不再进入 store state 的快照和调试链路。真正重新划清的边界是:
- 适合进 store 的,是
hasForm、筛选条件、选中项、布尔状态这类轻量数据 - 不适合进 store 的,是
Formily form、DOM 节点、带大量内部引用的运行时实例
这种改法本质上是在重新划清边界:轻状态进 store,重实例不要进会被调试链路持续观察的状态面。 当然,这也不代表把实例移出 state 就万事大吉了,后面依然要按组件生命周期处理好它的创建、读取和清理。
最后它又回到同一个页面,再用 Playwright 复验一遍。修完之后,页面打开时的内存降到了 165MB ~ 207MB ,30 秒左右会进一步降到 96MB 左右。也就是说,这不是"看起来像修好了",而是前后数据能直接对上。整个排查、修改和验证的闭环,大约就用了十来分钟。
总结
这次最值得复用的,不是一条具体的性能结论,而是这套工作方式:让大模型配合浏览器,自己完成复现、排查、修改和验证。
如果以后你也想照着做,输入里至少要把几件事说清楚:
- 页面和场景:到底是哪一页、哪个交互有问题
- 现象和数据:比如内存到多少、持续多久、有没有日志
- 已知边界:哪怕只是"大概率和查询表单模块有关",也很有用,能帮大模型减少很多不必要的查询路径
- 证据入口:日志文件、入口代码、复现路径
- 闭环要求 :别只让它分析,直接要求它用浏览器先复现,再定位,再修改,最后验证
如果你懒得自己组织,可以直接按这个最小模板来:
页面是一个带复杂查询联动的后台页面。
现象是页面打开后内存升到 900MB 左右,30 秒后才回落。
我已经确认问题大概率和查询表单有关。
证据入口是启动入口里的内存日志。
请你先用浏览器复现,再定位原因,改完以后给我修复前后的对比数据。
一句话说,这次真正能抄作业的地方就是:
把大模型当成一个会用浏览器、会自己验证结果的工程执行者,而不只是一个会聊天的分析助手。