One client. One React app, mostly generated by AI. Two years of "let's try it another way." This is the honest version, not the one in the case-study PDF, but we publish it anyway.
Key takeaways
- Vibe-coding cleanup is now a real engineering job: taking software someone generated fast with AI and making it hold up in production. I spent close to two years on one such project — an old Windows tool being reborn in React, most of the original built by a single person leaning on AI.
- The codebase had sat untouched for about three years. It ran three styling systems and three state-management systems at once, shipped ~135KB of gzipped dead weight, and choked on tablets under element trees of up to ~15,000 nodes.
- The wins were real: I cut the bundle ~30% and cut wasted re-renders by an estimated 40–60%.
- The root cause was never "the AI." It was a shifting product vision and no one owning it.
- Independent 2026 research agrees the mess is systemic: ~45% of AI-generated code fails security tests and AI-authored pull requests carry ~1.7× more issues. Salesforce analysts called 2026 "the year of technical debt."
The call
You can tell a lot about a codebase from the first hour.
I won't pretend this was a hero story, it was long and it was frustrating. But it's true — and in 2026, "I got hired to clean up what one person built with AI" isn't a rare sentence. It's close to a job title.
Cleanup is a job now
The context, briefly, because it's the whole reason this work exists. Developers have adopted AI coding tools almost universally — 84% now use or plan to use them, up from 76% a year earlier — while their trust in the output has fallen, from 40% to 29% in a single year, per Stack Overflow's 2025 survey. People are shipping code they increasingly don't trust, because the speed is too good to refuse.
The bill shows up later. Veracode tested over 100 models and found ~45% of AI-generated code introduces an OWASP Top 10 vulnerability — a security pass rate that has stayed flat near 55% even as the models got better at writing code that merely runs. CodeRabbit's analysis of 470 real pull requests found AI-co-authored changes carry ~1.7× more issues, concentrated in logic and error handling. Forrester expects 75% of technology decision-makers to be carrying moderate-to-severe technical debt by 2026, and Salesforce analysts named 2026 "the year of technical debt (thanks to vibe-coding)".
None of that is an argument against AI. It's a description of the gap someone gets hired to close. On this project, that someone was me.
The project
The app was a tool for engineers: a way to explore software architectures, map dependencies between components, track artifacts, and visualize code metrics. Originally it lived as a widget inside a Windows application, written in — as best I can tell — a very early version of PowerBuilder. The client wanted it reborn as a modern React app. Not a lazy port, either: he wanted to see and control what was happening on the Windows side, then analyze all of it on the React side. A bridge between a legacy world and a modern one.
The catch: most of the original had been generated by one person leaning on AI, as far as I could tell. By the time it reached me, the codebase had been untouched for about three years — I could read that straight off the commit history. In front-end, three years is a geological era.
What "AI-built" looks like under the hood
Everyone imagines AI code as either magic or garbage. The reality is more tiring: it runs. Then you open it and find a body with no skeleton.
The stack told the story before I read a single function. React 17 — two major versions behind by the time I arrived. TypeScript, but only where it was free. And then the part I still describe to people when they ask what vibe-coded software actually looks like at scale — the app was running everything three times over:
-
Three ways to style. styled-components (42 files), CSS Modules / SCSS (10 files), and MUI's
styled()(6 files). One component,Query.tsx, used all three at once. -
Three ways to manage state. Redux Toolkit (three slices —
analytics.tsalone ran 1,626 lines), plus eight React Context providers, plus an Apollo Client that was fully wired up and never used. Not one GraphQL query in the entire app.
That's the most honest description I have of this kind of software: it's not that any one part is done badly — it's that nothing is done well anywhere. Every choice was a compromise built around a previous choice, until you couldn't cleanly remove anything because something three files away depended on it.
The architecture isn't wrong. There just isn't one. That's what the AI never supplied, and what nobody added afterward.
The performance story, honestly
Here's the part people expect to be dramatic. The client wanted this running on tablets, and the app built element trees of up to ~15,000 nodes with the heavy math running client-side, on the device. On a desktop you might get away with it. A mid-range tablet is a far weaker machine, and you could feel the lag.
The physics are real: every node is work the browser has to lay out, paint, and composite, and React has to reconcile that tree against the previous one on each update — a cost that balloons as the tree grows. So I expected the fix to be some elegant rendering trick.
It wasn't. The profiler kept pointing somewhere far less elegant. The single biggest drag wasn't the tree — it was ~135KB of gzipped dead weight in the bundle (that unused Apollo stack alone, with graphql and subscriptions-transport-ws, was about 80KB), and a re-render storm: analytics.tsx held eight separate useState hooks, so every setter re-rendered the whole component and its children, while each of the eight Context providers re-rendered 15–30 consumers on any change.
So the work was debt paydown. I stripped the dead dependencies, consolidated the three state systems toward Redux (which batches updates), and pulled logic out into dedicated hooks. Here's what that actually moved, from my own before-and-after profiling on a mid-range Chrome/Android tablet:
- Bundle: ~450KB → ~315KB gzipped (−30%).
- Wasted re-renders: an estimated 40–60% fewer after the Redux migration and hook extraction. (That one's my estimate, not a clean measurement — re-render counts move around with usage.)
- Memory: ~180–220MB → ~150–180MB.
- First load: ~4–5s → ~3.5–4.5s on 3G/4G — the 135KB cut buys you maybe 100–200ms.
I'm giving you the honest numbers, not the marketing ones. Half a second off a cold load is not a headline. But it's real, it's measured, and it's what paying down this kind of debt actually looks like: no single heroic commit, just a lot of removing things that should never have been there.
The real root cause wasn't the AI
The AI didn't sink this project. Indecision did.
The client kept doing himself a disservice. We'd build one version — a month, two months of work — and then: "I don't like it, let's do it another way." A month after that, he'd finally work out what he'd actually wanted all along. Had that clarity existed on day one, most of the structural mess wouldn't have existed at all.
That's the sad thing under a lot of "AI ruined our codebase" stories. AI is a mirror. Point a shifting product vision at it and it will very efficiently generate a shifting product — three of them, stacked on top of each other, which is exactly how you end up with three styling systems and an ORM nobody calls.
People ask whether the fix is hiring a business analyst. If you want the business problem framed properly, yes. A developer can do that framing to save money, but only superficially — they'll see it through an engineering lens, not the end user's. The role you genuinely cannot skip is the client's own: someone has to hold a clear, stubborn vision of what the product is supposed to be at the end. No analyst and no engineer can install that for you.
Rebuild or rescue?
The question I get most: revive the old thing, or start over? The honest answer is that it's situational, and anyone with a universal rule is selling one. Sometimes there's a solid core worth building on, and throwing it out would be malpractice. This project wasn't that. If it had reached me as an audit before anyone committed to a direction, I'd have said start from scratch — and by the time I finished, there was barely anything of the original left alive anyway. The whole way through, my hands were tied by decisions that had already hardened.
Which is why the audit comes first, always: you can't make that call from the outside. For me it takes a week to a month to really understand a system — long enough to see past the surface people tidy up for visitors. One thing that made this one survivable: the client was a former programmer, so when I explained why something was hard, he believed me. That's rarer than it should be, and I got lucky.
Where Azati comes in
This kind of work has a name now, and it's most of what my team does: taking AI-fast code and legacy systems and making them production-grade without inheriting a fresh mess. The difference between vibe coding and engineering was never whether a machine wrote the code — it's the discipline that follows it: an audit before promises, review and quality gates tuned for AI's specific failure modes, and tests, docs and knowledge transfer treated as deliverables so nobody's left asking why the code works the way it does.
If any of the above sounded familiar, two of our public write-ups tell versions of the same story — a vibe-code rescue for a home-maintenance startup and an AI-accelerated real-estate catalog where early speed outran structure. It's the work behind our Vibe Coding Engineering, Legacy-to-AI Modernization and Custom Software Engineering practices.
Authorship stays with me — my project, my account. Written up with the Azati team.