1. The moment: when the app works on the outside but creaks on the inside
At first it's all magic. You tell the AI "add a profile screen" and it appears. "Now an export button," and it appears. You move fast, really fast. But there's a moment — almost always between the third and tenth week — when something shifts. Asking for a small feature is no longer instant. The AI takes longer, gets confused, touches things it shouldn't. Bugs pop up in places you never even touched. The app works, but moving it got heavy, like walking with mud on your shoes.
That's THE moment. Nothing dramatic happened: nothing visible broke. What happened is invisible and it's called technical debt. The AI built fast and, like anyone who builds fast, it left rubble behind: code nobody uses anymore but that's still there, the same logic copied across five files, any types that switch off every alarm, animations thrown together any which way. None of that shows up on screen. All of it makes every future change harder.
2. The pain: where it comes from and why AI makes a mess (even when it writes "well")
Here's the important, honest part: AI doesn't write bad code. It writes code that works. The problem is that it optimizes for "make it work now," not for "make it easy to maintain three months from now." And those two things pull in different directions. That's why, even though every piece is fine, the sum gets dirty. These are the four kinds of rubble it leaves, and why it leaves them:
- Dead code. You ask for a change, the AI rewrites a fresh function… but forgets to delete the old one. It stays there, orphaned, with nobody calling it. Multiply that by hundreds of changes and you've got entire files that no longer serve any purpose.
- Duplication. The AI doesn't always remember it already wrote that same logic before, so it writes it again. Now the same rule lives in five places. The day the rule changes, you have to fix it five times — and you always miss one.
- `any` and loose types. When the AI isn't sure of a value's type, it takes the shortcut: it marks it as
any. With that, it switches off every alarm TypeScript has for warning you about errors. The code compiles… and blows up in production. - Animations and effects thrown together roughly. The AI drops in animated gradients, blurs, shadows — they look good, but poorly built they make the GPU sweat, and the app feels slow on phones and modest laptops.
3. The 7 phases of the surgery (the heart of it all)
A surgeon doesn't cut open and poke around at random: they follow a protocol, in order, step by step. Code surgery is the same. There are seven phases, and the order matters: first you remove the dead code (so you don't clean up code you were going to delete), then you merge duplicates, then you harden animations, then React performance, then the types, then you calm the GPU down, and at the end you issue a health report. Here's each one.
Phase 1 · Remove dead code
The first thing is to clear off the table everything that no longer breathes. It's the most rewarding phase because it clears the ground for all the others: there's no point optimizing a function you're about to delete.
- Orphaned files — files nobody imports from anywhere. They're there from an old version that never got deleted.
- Exports with no consumers — functions or components that are exported but nobody uses. They got exported "just in case" and the just-in-case never came.
- Unused variables, functions and imports — declared and forgotten. The linter usually flags them, but the AI leaves them because "they don't bother anyone." They do bother: they're noise.
- Unreachable code branches — conditions that can never be true, code after a
return,ifstatements nobody triggers anymore.
Phase 2 · Audit duplicates
With the ground cleared, now you hunt for what's repeated. The rule of the trade is simple: if something shows up three times or more, it stops being coincidence and becomes a pattern that deserves a single place to live.
- Repeated logic → pull it out into a helper that both places call. One rule, one single place.
- Repeated magic numbers and strings (the same
48, the same URL, the same color) → centralize them into named constants. You change once, it changes everywhere. - JSX repeated three or more times (the same card, the same button with slight variants) → turn it into a subcomponent that receives its differences via props.
- The `useState` + its handler pattern, repeated (the same state with its update function copied across several components) → extract it into a custom hook that encapsulates the logic just once.
Phase 3 · Enforce the animation rules
Animations are where the AI improvises the most and where it leaves the most subtle glitches — things that don't break but that make the app feel "cheap": flickers, jumps, transitions that don't kick in. These are the concrete rules the surgery checks (they apply to React animation libraries like Motion / Framer Motion, the ecosystem standard):
- Animating to/from `transparent` can flicker → use
rgba(0,0,0,0)instead. When you interpolate the wordtransparentwith a color, the transition can produce an ugly flicker halfway through;rgba(0,0,0,0)interpolates cleanly. - No duplicate `transition` — an animation with two transition definitions fighting each other gives an unpredictable result. One single source of truth for the timing.
- Don't animate the `border` shorthand (the shorthand that bundles width, style and color) → animate separate properties like
borderColor. The shorthand doesn't interpolate cleanly. - `background` → `backgroundColor` in animations — animating the full
backgroundshorthand is problematic; animate only the specific property that changes. - Stable keys in lists that enter and exit (
AnimatePresence) — if the items in an animated list don't have a stable, uniquekey, the exit animations break and elements jump. The key must be a real id, never the array index.
Phase 4 · React performance
Here the surgery hunts for the work React repeats without need. Every unnecessary re-render is a little bit of slowness; added up, they make the app feel heavy. Four concrete things get reviewed:
- Handlers passed as props without `useCallback` → wrap them. Otherwise, every render creates a new function and forces child components to re-render even though nothing changed.
- Expensive computations without `useMemo` (filtering/sorting a big list, heavy calculations) → memoize them. Otherwise, they get recalculated on every render even when the data is identical.
- `useEffect` with badly declared dependencies — too many dependencies fire the effect for no reason; too few make it use stale data. Both are bugs. The dependency array must list exactly what the effect uses.
- `.map` without a stable `key` — rendering lists without a unique key (or using the index) makes React get confused when reordering and lose state. Each element needs its own id.
useCallback and useMemo "just in case." No: memoizing has a cost too (memory, complexity). The surgery only memoizes where there's a real, measurable benefit — a handler that flows down to many children, a genuinely expensive computation. Over-optimizing is its own kind of mess.Phase 5 · TypeScript hygiene
TypeScript is your code's alarm system: it warns you before an error reaches the user. But it only works if you let it see the types. Every any is an alarm someone switched off. This phase switches them back on.
- `any` → a real type, or `unknown` —
anydisables every check. If you truly don't know the type, useunknown, which forces you to verify it before using it (safe), instead ofany, which forces nothing (dangerous). - Review the `as Type` casts — telling TypeScript "trust me, this is of this type" is a promise that can be a lie. Every
asis a spot where the compiler stopped checking. Review them one by one: is it true, or is it a patch? - Internalize single-use exports — if a type or function is exported but only used within its own file, it shouldn't be public. Drop it to private: less surface, less noise.
- Type your component props — every component should declare which props it receives and of what type. Untyped props are
anyin disguise.
Phase 6 · Audit the GPU collapse
This is the phase almost nobody knows about and the one that shows up most on modest devices. Certain visual effects are gorgeous but brutally expensive to draw: if you overuse them, the graphics card gets saturated and the app stutters. The surgery hunts for the three classic culprits:
- Animated conic gradient (`conic-gradient`) → move it to a CSS animation with
@keyframes. Animating a conic gradient via JavaScript on every frame is dead expensive; with keyframes the browser optimizes it. - Lots of `repeat: Infinity` (several infinite-loop animations at once) → consolidate them. Ten endless animations running in parallel keep the GPU awake and burning battery nonstop. Merge them or cut the ones you don't really need.
- `backdrop-blur` on lots of small repeated elements → if the same background blur repeats across twenty little cards, the GPU recalculates it twenty times. For small, repeated elements, a solid (or semi-transparent) background looks almost the same and costs a fraction.
backdrop-blur, that trendy frosted-glass effect) forces the GPU to look at EVERYTHING behind the element and blur it in real time. One single big one, no problem. Twenty small ones repeating is like asking the card to blur the screen twenty times per frame. That's where your user's laptop starts spinning up the fan.Phase 7 · The health report
Every surgery ends with a report. Without a report you don't know what was touched, and you can't trust that there was no collateral damage. The last phase is the agent handing you a clear summary, in your own language, of everything it did:
- Count per phase — how many orphaned files, how many duplicates merged, how many
anyremoved, how many animations fixed, etc. - Lines purged — the total of dead code lines that came out. It's the most satisfying number: code you no longer have to maintain.
- A "production-ready" verdict — the explicit confirmation that nothing visual or behavioral was changed, only internal structure.
- What it did NOT touch and why — the things that looked like candidates but were deliberately left alone (duplicates that change for different reasons, memoize that wasn't worth it). The honesty of what it decided not to do.
4. The habit: surgery is maintenance, not a one-off operation
The most common mistake is thinking of surgery as something you do once, when everything is already a disaster. No. AI code gets dirty CONTINUOUSLY, because every build session leaves fresh rubble. Surgery isn't an emergency operation: it's the teeth cleaning you do every six months so you never end up needing a root canal.
- After a big build sprint — when the AI has just generated a lot of code all at once, that's the fresh rubble. Clean it before more piles on top of it.
- Before starting an important feature — give the AI a clean canvas to build on. On dirty code, it builds dirty.
- When you notice the AI starting to get confused — if the tool touches things it shouldn't or gets lost, that's a sign there's too much noise. Surgery gives it back its clarity.
- One file or module at a time, not the whole project at once — operating on a single module is safe and reviewable. Operating on everything at once is open-heart surgery without anesthesia. Go in parts.
5. The master prompt · hand it to your agent and operate phase by phase
Here's the key piece: ONE single prompt that turns your code agent into a surgeon who applies the 7 phases, in order, to a file or module — without touching anything visual. Fill in the [brackets], paste it, and let it operate. One tip before you start: make a commit on GitHub first (your safety net) and point it at ONE module, not the whole project.
Act as an expert debugging code surgeon. You are going to operate on this file/module: [FILE OR FOLDER PATH, e.g.: src/components/Dashboard/]. The stack is: [e.g.: React + TypeScript + Motion/Framer Motion + Tailwind]. SACRED, INVIOLABLE RULE: ZERO visual changes and ZERO behavioral changes. The app must look and behave EXACTLY the same before and after. You only clean and harden on the inside. In any doubt between cleaning more or preserving behavior, preserving behavior ALWAYS wins. Don't delete anything unless you're sure it's not used; flag it and ask me. Apply these 7 phases IN ORDER. Don't move to the next one without finishing the previous one. At the end of each phase, tell me in one line what you found and what you changed. PHASE 1 — DEAD CODE: remove orphaned files (that nobody imports), exports with no consumers, unused variables/functions/imports, and unreachable code branches. Before deleting anything doubtful, list it for me and wait for my confirmation. PHASE 2 — DUPLICATES: hunt for repeated logic (→ extract into a helper), repeated magic numbers/strings (→ named constants), JSX repeated 3+ times (→ subcomponent with props), and the repeated useState+handler pattern (→ custom hook). DON'T merge things that only look alike but change for different reasons. PHASE 3 — ANIMATION RULES: fix transparent→rgba(0,0,0,0), remove duplicate transition, don't animate the border shorthand (use borderColor), change background→backgroundColor in animations, and ensure stable, unique keys (never the index) in lists with AnimatePresence. PHASE 4 — REACT PERFORMANCE: wrap in useCallback the handlers passed as props, memoize expensive computations with useMemo, fix the useEffect dependency arrays (neither too many nor too few), and add a stable key to every .map. DON'T memoize just in case: only where there's a real benefit. PHASE 5 — TYPESCRIPT: replace each any with a real type or with unknown; review every 'as Type' cast (is it true or is it a patch?); internalize the exports only used within their own file; and type the props of every component. PHASE 6 — GPU COLLAPSE: move animated conic gradients to CSS @keyframes; consolidate the repeat:Infinity ones if there are many at once; and replace backdrop-blur with a solid/semi-transparent background on small elements that repeat many times. PHASE 7 — HEALTH REPORT: when you finish, hand me a report with: count per phase, total lines purged, the explicit confirmation that you didn't change anything visual or behavioral, and a list of what you decided NOT to touch and why. Work phase by phase, show me the diff of each one, and if anything puts the sacred rule at risk, STOP and ask me before continuing.
6. Verify the surgery didn't leave wounds
A surgery isn't over when you close up: it's over when you confirm the patient is fine. The sacred rule (zero behavioral changes) has to be checked, not just trusted. There are two automatic safety nets that confirm it for you in seconds, and that your agent can run without you programming anything.
# 1) The type check: zero errors = the surgery didn't break the contracts npx tsc --noEmit # 2) The tests: if they passed before and pass the same after, # behavior was preserved npm test
git diff. Reading rule: almost everything you see should be RED lines (deleted) or things moved around, not new logic. If you see user-facing text strings changed, different default values, or new conditions, there's a possible wound there — ask the AI why before accepting.7. The easiest paths · what the AI does by chat and what you decide
So it doesn't get complicated: most of the surgery is done by your code agent alone, through the chat. Your job is to steer and approve. Here's the split.
- Sweeping the module and detecting the dead code, the duplicates, the
anyand the badly built animations — you just give it the master prompt. - Applying the fixes phase by phase, showing you the diff of each one.
- Running
tscand the tests to confirm it didn't break anything. - Writing the final health report with the counts and what it decided not to touch.
- Making the safety commit BEFORE — your undo button. You do that yourself from GitHub Desktop or the chat, before operating.
- Approving the doubtful deletions — when the AI isn't sure whether something is used, it asks you; the final call is yours.
- Reviewing the diff — reading the changes and confirming there are no behavioral wounds before accepting.
- Deciding the scope — which module gets operated on and in what order. Don't delegate "operate on everything": go in parts.