●TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13●OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude Code●SIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App Store●MAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessage●NATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core ML●SEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round●TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13●OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude Code●SIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App Store●MAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessage●NATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core ML●SEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round
Diagnosing and Fixing Memory Leaks in Rork-Generated React Native Code with Flipper and Instruments
A field-tested playbook for diagnosing memory leaks in Rork-generated React Native apps, with concrete heap thresholds, before/after fixes for five common patterns, and the actual retention and eCPM impact measured on a wallpaper app that hit $7K/month in AdMob revenue.
I'm Hirokawa, an indie developer who has been shipping iOS and Android apps since 2014, with around 50M lifetime downloads across the portfolio. Once I started building experimental apps in Rork, the single thing I worry about most is memory hygiene. On a wallpaper app that was at one point earning more than $7K/month in AdMob revenue, I once missed a memory leak that dragged D7 retention down 18% and pulled my eCPM down 22% — because ad networks treat "app that crashes" as a downgrade signal for bidding.
Rork generates surprisingly good React Native + Expo code, but "code that runs" and "code that releases memory properly" are not the same. This guide covers the practical division of labor between Flipper and Xcode Instruments, before/after fixes for the five most common leak patterns I see in Rork output, and the concrete numerical thresholds I use to decide whether to ship or block a release. If you're using Rork Max and now have a real Xcode project in your hands, the Instruments section below is going to be particularly useful.
Why Rork-Generated Code Is Prone to Memory Leaks
Rork's generator is great at declarative hook-based React code but consistently skips the lifecycle bookkeeping. There are four recurring blind spots:
useEffect registers event listeners without returning a cleanup function
setInterval or setTimeout is used without clearing the timer on unmount
Async operations try to update state after the component has been unmounted
Supabase / Firebase realtime subscriptions and RevenueCat listeners are never unsubscribed
None of these are "broken" in the sense that the app won't run — but leave them in place long enough, and memory will quietly pile up. In my experience operating Dolice's wallpaper app portfolio, screens that users navigate frequently (even if individual sessions are short) suffer the most. When the heap grew by more than 5MB over 10 navigation round-trips, "slow" and "crashes" reviews started showing up almost immediately.
Release-Gating Thresholds from Operations Experience
Before the technical details, here are the numerical thresholds I use to decide whether a build is safe to ship. These came out of the period when one of my apps was clearing $7K/month on AdMob and I needed a repeatable rule.
Heap growth over 10 round-trips under 3MB → safe to ship
Growth 3–5MB → debug, identify the cause, fix if feasible, then ship
Growth over 5MB → block the App Store / Google Play submission until the cause is found
Growth over 10MB on an already-released build → emergency update within 48 hours
These numbers come from reverse-engineering when iOS kills backgrounded apps. On a 4GB RAM device, apps become OS-kill candidates roughly above 1.4GB resident, so a 100MB app burning through the remaining 1.3GB across navigation round-trips is the realistic concern. What matters is not the absolute heap size but the rate of growth.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Apply a clear release-gating rule (≥5MB heap growth over 10 navigation round-trips = block release) instead of vague 'it feels slow' judgments
✦Fix the five most common Rork leak patterns (useEffect, timers, async-after-unmount, Supabase/RevenueCat listeners, FlashList keyExtractor) with side-by-side before/after code
✦Use real numbers from an indie wallpaper app shipping ~50M lifetime downloads: leaking code dropped D7 retention 18% and AdMob eCPM 22% — fixing leaks is a revenue strategy, not just hygiene
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
The first step is confirming a leak exists. Guessing and patching code at random is not a good use of time.
Step 1: Setting Up Flipper (requires macOS)
# Download and install Flipper from https://fbflipper.com/# Connect via a development build, not EASnpx expo start --dev-client# Launch the Flipper Desktop app, then connect the simulator or a physical device# If auto-detection fails, add the device manually via "+ Add Device"
Open both the "React DevTools → Profiler" tab and the "Memory" tab in Flipper before starting any measurements.
Step 2: Run a Repeatable Measurement Protocol
A repeatable protocol is what makes leak fixes comparable. The "leak regression test" I run is:
Launch the app and idle on the home screen for 10 seconds
In the Memory tab, click "Force GC" to capture a baseline
Open the target screen → navigate back, repeated 10 times (3–5 seconds per round-trip)
Click "Force GC" again at the end
Record the delta between baseline and final heap size
Inserting Force GC at both ends is critical. Without it, garbage-collection-pending objects look like leaks, and you'll waste twice the time chasing false positives.
Step 3: Reading the Numbers
In the Memory tab:
Heap Size: Use the thresholds above. >5MB delta over 10 round-trips means block the release
Retained Size: If a particular component is much larger than the others, that's your suspect
GC Roots count: If this keeps climbing, listener cleanup is almost certainly missing
A clean, leak-free app should return to roughly its starting heap size after the protocol. If it doesn't, something is holding on to memory it should have released.
The Five Most Common Patterns (and How to Fix Them)
Pattern 1: Missing useEffect Cleanup
By far the most frequent issue in Rork-generated code. In my own audits, when I review 5 screens of Rork output, 3 of them have this pattern somewhere.
// ❌ Leaks memory — no cleanup functionuseEffect(() => { const subscription = someEventEmitter.addListener('update', handleUpdate); // This subscription outlives the component}, []);// ✅ Fixed — cleanup function returneduseEffect(() => { const subscription = someEventEmitter.addListener('update', handleUpdate); return () => { subscription.remove(); // Runs when the component unmounts };}, []);
When I ask Rork to fix this for me, I recommend the prompt "Add cleanup functions to all useEffect event listeners, but do not change the dependency arrays." Letting Rork modify the dependency arrays can introduce subtle re-render bugs in unrelated parts of the screen.
Pattern 2: Timer Not Cleared on Unmount
Common whenever you implement polling or delayed actions.
// ❌ Timer keeps firing after the screen is goneuseEffect(() => { const timer = setInterval(() => { fetchLatestData(); }, 5000); // Nothing stops this timer when the component unmounts}, []);// ✅ FixeduseEffect(() => { const timer = setInterval(() => { fetchLatestData(); }, 5000); return () => clearInterval(timer);}, []);
setTimeout follows the same rule: return () => clearTimeout(timer) in the cleanup. One trap to watch for: if you're using requestAnimationFrame in a loop (common for wallpaper-preview animations), make sure you also call cancelAnimationFrame(frameId) in the cleanup. This one bit me multiple times on the wallpaper app.
Pattern 3: State Updates After Unmount
If an API call or async operation finishes after the user has already left the screen, the callback will try to write to state in an unmounted component. React 18 stopped showing the console warning for this, but the wasted computation is still real.
// ✅ isMounted flag pattern — simple and readableuseEffect(() => { let isMounted = true; const fetchData = async () => { const result = await api.getItems(); if (isMounted) { setItems(result); } }; fetchData(); return () => { isMounted = false; };}, []);
You can also use AbortController, but the isMounted flag is easier to slot into existing Rork output without restructuring anything. In my experience, asking Rork to "protect async operations with the isMounted pattern" has a higher success rate than asking it to introduce an AbortController.
RevenueCat's customer-info listener has the same issue. If you register with Purchases.addCustomerInfoUpdateListener, you must remove it with Purchases.removeCustomerInfoUpdateListener. On one of my apps, leaving this in place added 4MB of monotonic heap growth per month.
Pattern 5: FlashList Key Collision After Migrating from FlatList
Strictly speaking this isn't a leak, but after switching from FlatList to FlashList you can see sudden heap spikes. FlashList recycles cells internally, so if keyExtractor returns a non-unique key, the same cell is retained multiple times and memory piles up.
// ❌ Duplicate IDs cause heap growth<FlashList data={items} keyExtractor={(item) => item.userId} // Duplicates when one user has multiple items estimatedItemSize={80} renderItem={renderItem}/>// ✅ Compound key guarantees uniqueness<FlashList data={items} keyExtractor={(item) => `${item.userId}-${item.timestamp}`} estimatedItemSize={80} renderItem={renderItem}/>
Keeping estimatedItemSize close to the median actual size (within ±5pt) is also important. Rork's default of 100 is rarely a good fit; tightening this gives the heap a noticeably steadier baseline.
Going Deeper with Xcode Instruments (iOS)
Once you've confirmed a leak with Flipper, Instruments gives you a richer picture on the iOS side.
Step 1: Launch and Configure Instruments
# With Rork Max, you already have a generated Xcode project# Open .xcworkspace in Xcode# Product → Profile (Cmd + I) → choose the Leaks template# After launch, click "+" in the bottom-left and add the Allocations template
Pairing Leaks with Allocations lets you track not just true leaks but also "lingering memory" — objects that still have valid references but should have been released. These don't trip the Leaks detector but cause real damage in production.
Step 2: Operational Protocol
Run the same 10-round-trip test under Instruments. After each round-trip, press "Mark Generation" (the flag icon) — this lets you see, generation by generation, which class instances are accumulating.
If a particular class count keeps climbing across generations, that's where your leak lives. On one of my apps, UIView subclasses were accumulating 3–5 per round-trip; the cause was a code path that wasn't calling removeFromSuperview.
Step 3: Interpreting Results and Next Actions
Instruments has a steeper learning curve than Flipper, but it tells you both "which object is not being released" and "where the references are still held." Use it whenever Flipper confirms a leak exists but can't pinpoint the cause.
Verifying Your Fixes
After making changes, always verify them with the same method:
Connect Flipper
Run the "leak regression test" protocol (Force GC → 10 round-trips → Force GC)
Confirm the Heap Size delta is under 3MB
On iOS, optionally run the Generations check in Instruments
As a final pre-submission check, run a 30-minute continuous-use test on a real device
If the heap returns to baseline, you're done. If it's still climbing, there's likely another instance of the same pattern elsewhere. You can ask Rork to scan the entire file: "Check all useEffect hooks in this file and add cleanup functions wherever they're missing. Only check — do not modify hooks I haven't asked you to."
A Prompt Template for Rork
If you'd rather have Rork handle the fixes directly, here's the prompt I use in production:
Review this file for memory leaks and fix all of the following:
- useEffect hooks that register event listeners without returning a cleanup function
- setInterval / setTimeout calls that aren't cleared on unmount
- requestAnimationFrame loops that aren't cancelled
- Supabase / Firebase realtime subscriptions that aren't unsubscribed
- RevenueCat addCustomerInfoUpdateListener that isn't removed
- FlashList keyExtractor returning non-unique values
- Async operations that might update state after the component unmounts
Constraints:
- Do not modify existing dependency arrays
- Prefer the isMounted flag pattern for async protection
- After fixing, list every change and the reason for it
Pair this prompt with a specific file path (e.g., src/screens/ChatScreen.tsx). One file at a time produces noticeably fewer misses than batching multiple files into a single request.
Impact on eCPM and Retention
Leaving leaks unaddressed isn't just a technical hygiene issue — it directly affects revenue. Here are the actual before/after numbers from the wallpaper app I mentioned earlier:
D1 retention: 38% → 41% (+3pt)
D7 retention: 11% → 13% (+2pt)
Average session length: 2m10s → 2m45s (+27%)
AdMob eCPM: $4.20 → $5.15 (+22%)
Monthly revenue: ~$6,500 → ~$8,000 (+22%)
AdMob tends to lower eCPM for apps with high short-session churn, because the auction algorithm reads "showing an ad doesn't lead to clicks or future sessions" as a signal to pay less. Retention loss and eCPM loss move together. Fixing leaks is a quality improvement and a revenue strategy.
Building the Habit Early
Memory leaks are much easier to prevent than to fix after the fact. A few habits to build into the Rork workflow:
Whenever you see a useEffect, immediately ask whether it needs a cleanup function
Treat any timer (setInterval, setTimeout, requestAnimationFrame) as automatically requiring a clear / cancel
Assume any external subscription (Supabase, Firebase, WebSocket, RevenueCat) needs an explicit unsubscribe
Use compound keys in FlashList keyExtractor to guarantee uniqueness
After changing a key screen, run the "10 round-trips + Force GC" test in Flipper as a baseline check
Insert one Instruments Generations check before App Store / Play submission
Across 50M lifetime downloads of personal apps, I keep coming back to the same lesson: taking memory hygiene seriously from day one cuts later operational cost by more than half. Apps that handle memory well feel noticeably snappier over long sessions — and users notice. Knowing how to diagnose and fix leaks means that when "the app gets slow" reports start coming in, you have a repeatable process instead of a guessing game.
Start by running a quick search for useEffect in your codebase and checking each one for a missing cleanup. There's a reasonable chance you'll find one or two right away.
Share
Thank You for Reading
Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.