1. 时机:应用外表能跑,内部却在吱嘎作响
一开始一切都像魔法。你对 AI 说"加一个个人资料页",它就出现了。"再来个导出按钮",它也出现了。你走得很快,非常快。但总有一个时刻——几乎都在第三周到第十周之间——某种东西变了。要一个小功能不再是瞬间完成。AI 变慢了,搞混了,动到了不该动的地方。在你根本没碰过的地方冒出 bug。应用能跑,但改动它变得沉重,就像鞋里进了泥在走路。
那就是那个时刻。没发生什么戏剧性的事:没有任何看得见的东西坏掉。发生的事是看不见的,它叫做技术债。AI 构建得快,而像所有构建得快的人一样,留下了瓦砾:没人再用却还在那里的代码,同一段逻辑被复制到五个文件里,让所有警报都失灵的 any 类型,随随便便搭起来的动画。这些在屏幕上都看不到。而这一切都让未来的每一次改动更加艰难。
2. 痛点:它从哪来,以及为什么 AI 会弄脏(哪怕它写得"好")
这里有个重要且诚实的点:AI 并不写烂代码。它写的是能跑的代码。问题在于它优化的是"现在能跑",而不是"三个月后好维护"。而这两件事往相反的方向拉扯。所以哪怕每一小块都没问题,加在一起还是会变脏。以下是它留下的四类瓦砾,以及它为什么会留下它们:
- 死代码。 你要一个改动,AI 重写了一个新函数……却忘了删掉旧的。它就留在那里,成了孤儿,没人调用。把这个乘以几百次改动,你就有了一整批毫无用处的文件。
- 重复。 AI 并不总是记得它之前已经写过同样的逻辑,于是又写了一遍。现在同一条规则住在五个地方。等这条规则要改的那天,得改五次——而且总会漏掉一个。
- `any` 和松散的类型。 当 AI 不确定某个数据的类型时,它走捷径:标成
any。这样它就关掉了 TypeScript 用来提醒你错误的所有警报。代码能编译……然后在生产环境里爆炸。 - 粗糙搭起来的动画和特效。 AI 放了动画渐变、模糊、阴影——看起来不错,但搭得糟糕就会让 GPU 出汗,应用在手机和普通笔记本上感觉很卡。
3. 手术的 7 个阶段(一切的核心)
外科医生不会打开后随便乱翻:他遵循一套流程,按顺序,一步一步来。代码手术也一样。一共七个阶段,而且顺序很重要:先清除死代码(这样才不用去清理你本来要删的代码),然后合并重复,然后加固动画,然后 React 性能,然后类型,然后安抚 GPU,最后出一份健康报告。下面是每一个阶段。
阶段 1 · 清除死代码
第一件事是把所有不再呼吸的东西从手术台上搬走。这是最划算的阶段,因为它为其余所有阶段清理出地盘:去优化一个你打算删掉的函数毫无意义。
- 孤儿文件 —— 没有任何地方 import 的文件。它们留在那里,来自一个从没删掉的旧版本。
- 没有消费者的 export —— 导出了却没人用的函数或组件。它们"以防万一"被导出,而那个万一从没到来。
- 未使用的变量、函数和 import —— 声明了又被遗忘。linter 通常会标出来,但 AI 把它们留着,因为"它们不碍事"。它们碍事:它们是噪音。
- 无法到达的代码分支 —— 永远不可能为真的条件、
return之后的代码、再也没人触发的if。
阶段 2 · 审计重复
地盘清理干净后,现在你要找重复的东西。这行的规则很简单:如果某样东西出现三次或以上,它就不再是巧合,而成了一个模式,值得有一个唯一的落脚地。
- 重复的逻辑 → 抽到一个辅助函数(
helper),让两处都调用它。一条规则,一个落脚地。 - 重复的魔法数字和文本(同一个
48、同一个 URL、同一个颜色)→ 把它们集中到有名字的常量里。改一次,处处都改。 - 重复三次或以上的 JSX(同一张卡片、带轻微变体的同一个按钮)→ 把它变成一个通过 props 接收差异的子组件。
- 重复的 `useState` + 它的 handler 这一模式(同一个状态和它的更新函数被复制到多个组件里)→ 把它抽到一个自定义 hook(
custom hook)里,把逻辑封装一次。
阶段 3 · 遵守动画规则
动画是 AI 最爱即兴发挥、也最容易留下细微毛病的地方——那些不会崩坏、却让应用感觉"廉价"的东西:闪烁、跳动、启动不了的过渡。以下是手术要核对的具体规则(适用于 React 的动画库,比如 Motion / Framer Motion,这是生态里的标准):
- 从/向 `transparent` 做动画可能闪烁 → 改用
rgba(0,0,0,0)。当你把transparent这个词和一个颜色做插值时,过渡在中途可能出现难看的闪烁;rgba(0,0,0,0)插值得干净。 - 没有重复的 `transition` —— 一个动画有两处相互打架的 transition 定义会给出不可预测的结果。计时只能有一个唯一的真相来源。
- 不要给简写属性 `border` 做动画(那个把粗细、样式和颜色合在一起的简写)→ 给分开的属性做动画,比如
borderColor。简写属性插值不干净。 - 动画里 `background` → `backgroundColor` —— 给完整的简写
background做动画是有问题的;只给发生变化的那个具体属性做动画。 - 在进出的列表里用稳定的 key(
AnimatePresence)—— 如果一个动画列表里的元素没有稳定且唯一的key,退出动画就会崩坏、元素会跳动。key 必须是一个真实的 id,绝不能是数组的索引。
阶段 4 · React 性能
这里手术要找 React 没必要重复做的活儿。每一次不必要的重渲染都是一点点卡顿;加起来,就让应用感觉沉重。要检查四件具体的事:
- 作为 props 传下去、却没有 `useCallback` 的 handler → 把它们包起来。否则每次渲染都创建一个新函数,逼着子组件重渲染,哪怕什么都没变。
- 没有 `useMemo` 的昂贵计算(过滤/排序一个大列表、繁重的运算)→ 把它们 memo 化。否则每次渲染都会重新计算,哪怕数据一模一样。
- 依赖声明错误的 `useEffect` —— 依赖多了会无缘无故触发 effect;依赖少了会让它用到旧数据。两者都是 bug。依赖数组必须精确列出 effect 用到的东西。
- 没有稳定 `key` 的 `.map` —— 渲染列表时没有唯一 key(或用了索引)会让 React 在重排时搞混、丢失状态。每个元素都需要它自己的 id。
useCallback 和 useMemo。不:memo 化本身也有代价(内存、复杂度)。手术只在有真实且可衡量收益的地方 memo 化——一个传给很多子组件的 handler、一个真正昂贵的计算。过度优化是它自己的一种脏。阶段 5 · TypeScript 卫生
TypeScript 是你代码的警报系统:它在错误抵达用户之前提醒你。但它只有在你让它看到类型时才管用。每一个 any 都是有人关掉的一个警报。这个阶段把它们重新打开。
- `any` → 一个真实的类型,或 `unknown` ——
any关掉所有检查。如果你真的不知道类型,用unknown,它逼你在使用前先做校验(安全),而不是any,它什么都不逼(危险)。 - 审查 `as 类型` 的强制转换 —— 对 TypeScript 说"相信我,这是这个类型"是一个可能是谎言的承诺。每一个
as都是编译器停止检查的一个点。要一个一个审:它是真的,还是一个补丁? - 把只用一次的 export 内部化 —— 如果一个类型或函数被导出、却只在它自己的文件里用,它就不该是公开的。把它降为私有:更少的表面积,更少的噪音。
- 给组件的 props 加类型 —— 每个组件都该声明它接收哪些 props、是什么类型。没类型的 props 就是伪装的
any。
阶段 6 · 审计 GPU 崩溃
这是几乎没人知道的阶段,也是在普通设备上最明显的一个。某些视觉特效美得很,但绘制起来贵得惊人:如果你滥用它们,显卡就会饱和,应用就一顿一顿的。手术要找三个经典的罪魁祸首:
- 做动画的锥形渐变(`conic-gradient`) → 把它挪到用
@keyframes的 CSS 动画里。用 JavaScript 在每一帧给锥形渐变做动画贵得离谱;用 keyframes 浏览器会帮它优化。 - 很多 `repeat: Infinity`(好几个无限循环动画同时跑)→ 把它们合并。十个永恒动画并行跑着会让 GPU 一直醒着、不停烧电池。把它们合起来,或者减到真正需要的那些。
- 在很多重复的小元素上用 `backdrop-blur` → 如果同一个背景模糊在二十张小卡片上重复,GPU 就重算二十次。对小而重复的元素,一个纯色背景(或半透明的)看起来几乎一样,却只花一小部分代价。
backdrop-blur,那个很流行的磨砂玻璃效果)逼着 GPU 看清元素背后的所有东西并实时把它糊掉。单独一个大的,没事。二十个小的重复着,就像要求显卡每一帧把屏幕糊二十次。就是在那里,你用户的笔记本开始吹风扇。阶段 7 · 健康报告
每台手术都以一份报告结束。没有报告你就不知道动了什么,也没法确信没有附带损伤。最后一个阶段,是让 agent 用你的语言给你交一份清晰的总结,列出它做的一切:
- 按阶段计数 —— 多少个孤儿文件、合并了多少个重复、删掉了多少个
any、修正了多少个动画,等等。 - 清除的行数 —— 被搬走的死代码总行数。这是最让人满足的数字:你再也不用维护的代码。
- "可上生产"的裁决 —— 明确确认没有改动任何视觉或行为,只改了内部结构。
- 它没动的东西以及原因 —— 那些看起来像候选、却被故意留下的东西(因不同原因而改变的重复、不值得的 memo 化)。诚实地说明决定不做什么。
4. 习惯:手术是保养,不是一次性的手术
最常见的错误是把手术当成一件一切都乱成一团后才做一次的事。不是的。AI 代码持续变脏,因为每一次构建会话都留下新的瓦砾。手术不是急诊手术:它是你每六个月做一次、好让你永远不用做根管治疗的洗牙。
- 一次大的构建冲刺之后 —— 当 AI 刚刚一口气生成了大量代码,那就是新鲜的瓦砾。趁它上面还没堆更多东西前清掉它。
- 开始一个重要功能之前 —— 给 AI 一块干净的画布去构建。在脏代码上,它会构建出脏的。
- 当你注意到 AI 开始犯迷糊 —— 如果工具动了不该动的东西或迷了路,那是噪音太多的信号。手术把清晰还给它。
- 一次一个文件或模块,而不是整个项目一口气 —— 只做一个模块是安全且可审查的。一次做全部是没有麻醉的开胸手术。分批来。
5. 主 prompt · 交给你的 agent,一阶段一阶段动刀
这里是关键的一块:一个 prompt,把你的代码 agent 变成一名外科医生,按顺序把这 7 个阶段应用到一个文件或模块上——不动任何视觉的东西。填好 [方括号],粘贴进去,让它动刀。动手前一个建议:先在 GitHub 做一次 commit(你的安全网),并把它对准一个模块,而不是整个项目。
扮演一名精于调试的代码外科医生。你要给这个文件/模块动刀:[文件或文件夹路径,例:src/components/Dashboard/]。技术栈是:[例:React + TypeScript + Motion/Framer Motion + Tailwind]。 神圣且不可违背的规则:零视觉改动、零行为改动。应用在前后必须看起来和表现得一模一样。你只清理和加固内部。在"多清一点"和"保住行为"之间有任何犹豫时,永远是保住行为赢。如果你不确定某样东西是否被使用,就不要删它;把它标出来问我。 按顺序应用这 7 个阶段。不完成上一个就不要进入下一个。在每个阶段结束时,用一行告诉我你发现了什么、改了什么。 阶段 1 —— 死代码:清除孤儿文件(没人 import 的)、没有消费者的 export、未使用的变量/函数/import,以及无法到达的代码分支。删除任何可疑的东西之前,把它列给我并等我确认。 阶段 2 —— 重复:找重复的逻辑(→ 抽到一个 helper)、重复的魔法数字/文本(→ 有名字的常量)、重复三次以上的 JSX(→ 带 props 的子组件),以及重复的 useState+handler 模式(→ custom hook)。不要合并那些只是看起来像、却因不同原因而改变的东西。 阶段 3 —— 动画规则:修正 transparent→rgba(0,0,0,0),清除重复的 transition,不要给简写 border 做动画(用 borderColor),动画里把 background→backgroundColor,并确保带 AnimatePresence 的列表里 key 稳定且唯一(绝不用索引)。 阶段 4 —— React 性能:把作为 props 传下去的 handler 包进 useCallback,用 useMemo memo 化昂贵的计算,修正 useEffect 的依赖数组(不多不少),并给每个 .map 加稳定的 key。不要以防万一去 memo:只在有真实收益的地方做。 阶段 5 —— TypeScript:把每个 any 换成真实类型或换成 unknown;审查每个 'as 类型' 强制转换(它是真的还是一个补丁?);把只在自己文件里用的 export 内部化;并给所有组件的 props 加类型。 阶段 6 —— GPU 崩溃:把做动画的锥形渐变挪到 CSS @keyframes;如果有很多 repeat:Infinity 同时跑就合并它们;并在重复很多次的小元素上把 backdrop-blur 换成纯色/半透明背景。 阶段 7 —— 健康报告:结束时,交给我一份报告,包含:按阶段计数、清除的总行数、明确确认你没改动任何视觉或行为,以及一份你决定不动的东西及原因的清单。 一阶段一阶段地做,给我看每一个阶段的 diff,如果有什么会危及神圣规则,就停下来在继续前问我。
6. 验证手术没留下伤口
一台手术不是在你缝合时结束:它是在你确认病人无恙时结束。那条神圣规则(零行为改动)必须去验证,而不只是相信它。有两张自动的安全网能在几秒内替你确认,而且你的 agent 可以跑它们,你完全不用编程。
# 1) 类型检查:零错误 = 手术没破坏契约 npx tsc --noEmit # 2) 测试:如果之前通过、之后也照样通过, # 行为就被保住了 npm test
git diff 看一看它的 diff(改动)。阅读规则:你看到的几乎所有东西都该是红色的行(被删的)或位置移动,而不是新逻辑。如果冒出用户能看到的、被改动的文本字符串,不同的默认值,或者新的条件,那里就有一处可能的伤口——在接受前问问 AI 为什么。7. 最省事的路径 · AI 在聊天里做什么、你来决定什么
为了不把事情搞复杂:手术的大部分由你的代码 agent 自己在聊天里完成。你的活儿是指挥和批准。分工如下。
- 扫过模块,检测出死代码、重复、
any和搭得糟的动画——你只需给它主 prompt。 - 一阶段一阶段地应用修正,给你看每一个的 diff。
- 跑
tsc和测试,确认它没破坏任何东西。 - 写出最终的健康报告,附上计数和它决定不动的东西。
- 动手前做那次安全 commit —— 你的撤销按钮。这个由你在动刀前从 GitHub Desktop 或聊天里做。
- 批准可疑的删除 —— 当 AI 不确定某样东西是否被使用时,它会问你;最终决定是你的。
- 审查 diff —— 读一读改动,在接受前确认没有行为上的伤口。
- 决定范围 —— 动哪个模块、按什么顺序。别把"全动了"委托出去:分批来。