Shipping MOBA: Cleaning Up All the Vibe Coding
Cleaning Up All the Vibe Coding
How we removed 6,400+ lines of dead code and made our app faster
The Problem with "Vibe Coding"
You know how it goes. You're in the zone, ideas flowing, features materializing at the speed of thought. You build a social feed feature at 2 AM because "it would be cool." You create three different Magic 8 Ball components because you weren't sure which animation felt right. You scatter console.log statements everywhere because debugging is life.
This is vibe coding—the beautiful chaos of rapid prototyping where momentum matters more than maintenance.
But then you ship. And suddenly that beautiful chaos becomes... just chaos.
After months of vibe coding on MOBA (our React Native fitness & nutrition app), we woke up to a codebase that had:
- 350+ console.log statements bleeding into production
- 18 completely unused files including an entire social feed feature we never launched
- Duplicate components doing the same thing with different names
- Sequential database calls that could easily be parallelized
It was time for a cleanup.
The Numbers Don't Lie
Before vs. After
| Metric | Before | After | Change |
|---|---|---|---|
| TypeScript Lines | 46,139 | 42,546 | -3,593 lines |
| Source Files | 172+ | 140 | -32 files |
| Console.log Statements | 350+ | 0 (prod) | -100% |
| Files Modified | - | 118 | - |
| Commits | - | 76 | - |
Bottom line: We deleted 7.8% of the entire codebase and the app got faster.
What We Actually Did
Phase 1: The Console.log Massacre (350+ statements)
The first rule of production is: don't console.log in production.
We created a simple logger utility that respects React Native's __DEV__ flag:
// lib/logger.ts
const isDev = __DEV__;
export const logger = {
log: (...args: any[]) => isDev && console.log(...args),
warn: (...args: any[]) => isDev && console.warn(...args),
error: (...args: any[]) => console.error(...args), // Always log errors
debug: (...args: any[]) => isDev && console.debug(...args),
};
Then we systematically replaced every console.log across 37 files. This wasn't just about code hygiene—console operations are synchronous I/O that blocks the JavaScript thread. In production, we were literally logging ourselves into performance problems.
Result: Zero production console overhead.
Phase 2: The Memoization Sprint
React re-renders are the silent performance killer. We audited our component tree and found:
- Stateless components re-rendering on every parent update
- Expensive calculations running on every render
- Event handlers being recreated on every render (breaking
React.memo)
Components wrapped with React.memo:
NutritionBadge- displays macro calculationsInsightCard- rendered in listsNLSScoreDisplay- complex UI with multiple calculationsNLSDailyBreakdown- chart componentNLSMilestonesWidget- achievement badgesNLSInsightsPanel- parent containerProgressRing- animated componentNoGoalsPrompt,NoStravaPrompt- static contentSearchDrawer- modal component
Callbacks optimized with useCallback:
handleRefresh- pull-to-refresh handlerhandleInputMethodSelect- food input togglehandleMealPress- meal selectionhandleEditMeal- meal editing
Result: Fewer unnecessary re-renders in core components (observable in React DevTools, not formally benchmarked).
Phase 3: The Async Awakening
This one hurt to find. Our main food logging page had this pattern:
// 🐌 Before: Sequential calls (~800ms)
const profile = await getUserProfile(userId);
const goals = await getNutritionGoals(userId);
const preferences = await getMealPreferences(userId);
const history = await getTodayHistory(userId);
Four database calls. Four round trips. Four times the latency.
// 🚀 After: Parallel calls (~200ms)
const [profile, goals, preferences, history] = await Promise.all([
getUserProfile(userId),
getNutritionGoals(userId),
getMealPreferences(userId),
getTodayHistory(userId),
]);
Same data. Same reliability. Up to 4x faster (in theory—sequential calls become parallel).
Phase 4: The Great Deletion
This was the cathartic part. We systematically audited every file and asked: "Is this actually used?"
The Social Feed That Never Was:
We built an entire social feed feature with posts, comments, likes, and engagement bars. It was never integrated into the app. Eight files. 1,350 lines. Gone.
| File | Lines | Purpose |
|---|---|---|
FeedList.tsx |
200 | Social feed container |
PostCard.tsx |
200 | Individual post component |
CreatePostModal.tsx |
200 | New post modal |
CommentSection.tsx |
150 | Comments UI |
CommentInput.tsx |
100 | Comment input box |
EngagementBar.tsx |
100 | Like/comment buttons |
SearchFeedModal.tsx |
150 | Feed search |
feedService.ts |
250 | Backend service |
Duplicate Components:
We had THREE Magic 8 Ball components (landed on ImmersiveMagic8Ball), TWO settings panels, and TWO search modals. Each duplicate deleted.
Unused Utilities:
backgroundInsightService.ts- Built for background processing, never connectedxstorage.ts- Alternative storage wrapper, never usednlsDiagnostics.ts- Debug utility that lived in docs onlyusdaFoodData.ts- USDA API wrapper we never calledunits.ts- Unit conversion for a water logging feature we scrapped
Dead Infrastructure: The Supabase-to-Render Migration
This one is a classic vibe coding artifact. When we first built MOBA, we used Supabase Edge Functions for server-side operations—things like exchanging OAuth tokens with Strava and searching the web for nutrition data. Edge Functions are great: they're serverless, they scale automatically, and they live right next to your database.
But as the app grew, we needed more control. We wanted:
- Better logging and debugging
- Custom middleware (rate limiting, auth verification)
- The ability to chain multiple API calls without cold start penalties
- A single codebase for all our backend logic
So we migrated to a dedicated Express backend hosted on Render. We wrote new routes, tested them, deployed them, updated the app to point to the new endpoints... and then promptly forgot the old edge functions existed.
For months, these files sat in our repo:
supabase/functions/strava-exchange/- 107 lines of Deno codesupabase/functions/nutrition-search/- 267 lines of Deno code
They weren't hurting anything—they weren't even deployed anymore. But they were:
- Showing up in searches when we looked for "strava" or "nutrition"
- Confusing anyone reading the codebase ("wait, which one is the real endpoint?")
- Adding to our mental model of "stuff we have to maintain"
The fix was simple: rm -rf supabase/functions/. 374 lines gone.
The lesson: When you migrate infrastructure, delete the old stuff. Don't leave it "just in case." Git remembers. You don't need two implementations of the same thing cluttering your repo.
Also removed in this pass: a test page that was never navigated to (test-components.tsx), a dev script (add-mock-quick-add-meals.ts), 5 debug screenshots committed to root, an outdated debug guide, and Bolt.new config from the original scaffold.
Database Types Cleaned:
Removed Post, Comment, and UserPreview interfaces from our TypeScript definitions. The database tables still exist (migrations are permanent), but the app no longer pretends it uses them.
The Verification Process
Every cleanup was verified before committing:
- TypeScript Check:
npx tsc --noEmit- Catch type errors immediately - ESLint:
npx eslint . --ext .ts,.tsx- Code quality gates - E2E Tests: 17 Playwright tests covering:
- App launch and navigation
- Real login with test credentials
- Food logging flow
- Insights page rendering
- MOBA AI chat interaction
- Profile and NLS score pages
We ran the full E2E suite after every significant change. Any test failure meant immediate revert and investigation.
Lessons Learned
1. Vibe coding is fine. Vibe shipping is not.
There's nothing wrong with rapid prototyping. Build fast, experiment freely, throw things at the wall. But before you ship, take a beat. Delete the experiments that didn't pan out.
2. console.log is a code smell at scale
One or two? Fine. Three hundred and fifty? You're debugging in production, and your users are paying the performance tax.
3. Parallel async is almost always better
If your awaits don't depend on each other, they should be parallel. This is free performance.
4. Unused code is negative value
It's not "there if we need it later." It's:
- Cognitive load when reading
- Surface area for bugs
- Build time overhead
- False positives in searches
Delete it. Git remembers.
5. Small, atomic commits enable safe rollback
Every change was one commit. If something broke three commits later, we could cherry-pick out exactly the bad change. The 75 commits on this cleanup branch are individually revertible.
The Cleanup Checklist
If you're facing your own vibe-coding reckoning, here's the playbook:
- Create a dedicated cleanup branch
- Establish a TypeScript baseline (
npx tsc --noEmit | wc -l) - Create a logger utility with dev-only output
- Replace all
console.logstatements - Audit components for memoization opportunities
- Find sequential awaits that can be parallelized
- Grep for every file import—unused files have no imports
- Delete ruthlessly, verify continuously
- Run E2E tests after every significant change
- Commit small, push often
Final Stats
118 files changed
4,714 insertions (+)
6,436 deletions (-)
────────────────────
Net: -1,722 lines
Net reduction: 1,722 lines (after adding tests, the logger utility, and merging main)
Gross deletion: 6,436 lines of code that shouldn't have shipped
Percentage of codebase cleaned: 7.8%
Time invested: ~4 hours
Time saved (estimated): Every future developer who doesn't have to read thousands of lines of unused code.
Conclusion
Vibe coding built MOBA. Disciplined cleanup made it shippable.
The two aren't opposites—they're phases. Build fast, then clean ruthlessly. Your future self (and your app's performance) will thank you.
*This cleanup was performed on the MOBA React Native app using TypeScript, Expo, and Playwright for E2E testing.