Engineering

Shipping MOBA: Cleaning Up All the Vibe Coding

January 21, 20269 min read
React NativeTypeScriptPerformanceCode QualityRefactoring

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 calculations
  • InsightCard - rendered in lists
  • NLSScoreDisplay - complex UI with multiple calculations
  • NLSDailyBreakdown - chart component
  • NLSMilestonesWidget - achievement badges
  • NLSInsightsPanel - parent container
  • ProgressRing - animated component
  • NoGoalsPrompt, NoStravaPrompt - static content
  • SearchDrawer - modal component

Callbacks optimized with useCallback:

  • handleRefresh - pull-to-refresh handler
  • handleInputMethodSelect - food input toggle
  • handleMealPress - meal selection
  • handleEditMeal - 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 connected
  • xstorage.ts - Alternative storage wrapper, never used
  • nlsDiagnostics.ts - Debug utility that lived in docs only
  • usdaFoodData.ts - USDA API wrapper we never called
  • units.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 code
  • supabase/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:

  1. TypeScript Check: npx tsc --noEmit - Catch type errors immediately
  2. ESLint: npx eslint . --ext .ts,.tsx - Code quality gates
  3. 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.log statements
  • 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.