●PLAY — Google Play's target API level 36 requirement took effect yesterday, August 31. From today, new apps and updates must target Android 16●VISIBILITY — Apps still on API 35 stay listed but disappear for users on newer Android versions. No error is raised; new installs simply fade, which makes the change easy to miss●EXTENSION — If you missed the deadline, an extension through November 1, 2026 can be requested in Play Console — best filed alongside a concrete migration plan●APPLE — On the Apple side, the event lands September 9 and iOS 27 is reported to ship September 14. Testing generated apps on iOS 27 hardware before release week is time well spent●EXPO — Expo released expo-paste-input on August 28, a native module that brings image, GIF, and sticker paste to React Native TextInput●EAS — EAS Observe reached general availability on August 20, putting crash and performance monitoring on the same EAS platform as builds and updates●PLAY — Google Play's target API level 36 requirement took effect yesterday, August 31. From today, new apps and updates must target Android 16●VISIBILITY — Apps still on API 35 stay listed but disappear for users on newer Android versions. No error is raised; new installs simply fade, which makes the change easy to miss●EXTENSION — If you missed the deadline, an extension through November 1, 2026 can be requested in Play Console — best filed alongside a concrete migration plan●APPLE — On the Apple side, the event lands September 9 and iOS 27 is reported to ship September 14. Testing generated apps on iOS 27 hardware before release week is time well spent●EXPO — Expo released expo-paste-input on August 28, a native module that brings image, GIF, and sticker paste to React Native TextInput●EAS — EAS Observe reached general availability on August 20, putting crash and performance monitoring on the same EAS platform as builds and updates
Diagnosing and Fixing Memory Leaks in Rork-Generated React Native Code with React Native DevTools and Instruments
A field-tested workflow for diagnosing memory leaks in Rork-generated React Native apps using the React Native DevTools Memory panel and Xcode Instruments, with concrete heap thresholds and before/after fixes for five recurring patterns. Rebuilt for the post-Flipper toolchain.
After about twenty round-trips through a wallpaper app's preview screen, the phone was noticeably warm. Nothing crashed. Scrolling just caught, very slightly, under my thumb. I once let that "very slightly" slide, and watched D7 retention fall 18% while AdMob eCPM followed it down 22%. Ad networks treat an app that people bounce out of as one worth bidding less on, and the numbers made that lesson concrete.
Rork produces React Native + Expo code that genuinely runs. But "code that runs" and "code that releases memory" are two different things. The declarative parts come out clean; the teardown is what goes missing.
There's one more thing worth saying up front. This article originally assumed Flipper, and that assumption no longer holds. React Native deprecated its Flipper integration in 0.73, removed it from new app templates in 0.74, and made React Native DevTools the default debugger from 0.76 onward. Expo dropped Flipper support back in SDK 50. Most of the "profile memory with Flipper" walkthroughs still floating around won't even get you to a connected session today. What follows is the replacement workflow.
Why Rork-Generated Code Is Prone to Memory Leaks
Rork's generator handles declarative hook-based React well, but consistently skips lifecycle bookkeeping. Four blind spots come up again and again:
useEffect registers event listeners without returning a cleanup function
setInterval or setTimeout runs without being cleared on unmount
Async work tries to update state after the component has unmounted
Supabase / Firebase realtime subscriptions and RevenueCat listeners are never removed
The awkward part is that all four still work. Memory accumulates quietly over a long session until the OS kills the app on foreground return — which the user experiences simply as "it crashed." Apps with short sessions but heavy in-app navigation, like wallpaper apps, get hit hardest. Once a screen was leaking more than 5MB per ten navigations, "slow" and "crashes" showed up in the reviews almost immediately.
Release-Gating Thresholds
Before the tooling, here's where I draw the line. Moving from Flipper to React Native DevTools didn't change these numbers at all, because what's being measured is JS heap growth, not the tool.
Heap growth after 10 round-trips is under 3MB → ship it
Growth of 3–5MB → debug, find the cause, fix it, then ship
Growth over 5MB → block the release. No App Store or Play submission until the cause is identified
Growth over 10MB → if it's already live, ship an emergency update within 48 hours
These come from working backwards from device RAM and OS memory-warning behavior. On a 4GB iOS device, a foreground app becomes a termination candidate somewhere past roughly 1.3–1.4GB. An app starting at 100MB and adding 5MB per ten round-trips needs about 2,600 round-trips to get there on paper — but image caches and native allocations stack on top, so perceived degradation starts an order of magnitude earlier.
Watch the rate of growth, not the absolute number. That's the part worth internalizing. A leak-free app returns to roughly its starting heap no matter how many times you repeat the same navigation. Anything that doesn't return is a leak.
✦
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
✦Walk through heap snapshot comparison in React Native DevTools to pinpoint the exact retaining class, now that Flipper is gone from the toolchain
✦Turn vague performance anxiety into a shippable rule: 5MB of heap growth over 10 navigation round-trips blocks the release
✦Fix the five recurring Rork leak patterns (useEffect, timers, async-after-unmount, Supabase/RevenueCat listeners, FlashList keys) with side-by-side before/after code
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.
Profiling with the React Native DevTools Memory Panel
Step one is confirming that a leak actually exists. Fixing on instinct never ends.
Step 1: Open DevTools
If your app runs on Hermes, there's nothing extra to install.
# Expo projectnpx expo start# With the device connected, return to the terminal and press j to open DevTools# (For a bare React Native project: npx react-native start, then j)# The flow is identical when using a dev clientnpx expo start --dev-client
What opens is the Chrome DevTools frontend rather than a separate desktop app. Setup friction has essentially disappeared. If you ever lost half a day to mismatched Flipper plugin versions, this change lands as a relief.
You'll work in the Memory and Performance tabs. For leak diagnosis, the Memory tab's "Heap snapshot" is the main tool.
Step 2: Fix the Measurement Protocol
Running the same sequence every time is what makes results comparable. I keep this as a "leak regression test":
Launch the app and wait 10 seconds on the home screen
Take one heap snapshot in the Memory tab — this is the baseline
Open the target screen and go back, ten times, at 3–5 second intervals
Take a second snapshot
Compare the two
Taking a snapshot triggers garbage collection automatically, so there's no separate Force GC step the way there was in Flipper. One less thing to remember.
Step 3: Reading the Snapshots
Open the second snapshot, switch the dropdown at the top to Comparison, and set the first snapshot as the baseline. This is where the work happens.
Sort by Delta, descending. Constructors that grow with every round-trip float to the top
Look for classes whose # Delta is a clean multiple of your round-trip count. One leaked object per navigation shows up as 10; two shows up as 20
Expand the row and read the Retainers to see what's still holding the object
That "multiple of the round-trip count" heuristic is what actually cracks these cases. Staring at total heap size tells you nothing about who's responsible, but a class sitting at exactly 10 or 20 is almost certainly failing to release one object per navigation. Following the retainers usually lands on a listener or a timer closure that was never torn down.
The JS heap timeline in the Performance tab pairs well with this — seeing the staircase pattern line up with your navigation count makes the culprit screen obvious.
The Five Recurring Patterns
Pattern 1: Missing useEffect Cleanup
The single most common issue in Rork output. Reviewing five generated screens, this typically shows up in three of them.
When asking Rork to fix this, I phrase it as: "Add cleanup functions to the event listeners inside useEffect. Do not change the contents of the dependency arrays." Letting the model rewrite dependency arrays tends to introduce rendering bugs that are harder to trace than the leak you started with.
Pattern 2: Uncleared Timers
Common wherever polling or delayed work is involved.
The same applies to setTimeout with clearTimeout(timer). Loops built on requestAnimationFrame are the ones people forget to cancelAnimationFrame — I hit that repeatedly in wallpaper preview animations. Short animations mask the symptom, which is exactly why it goes unnoticed.
Pattern 3: State Updates After Unmount
If a user leaves the screen before an API call resolves, the callback tries to write state into an unmounted component. React 18 stopped warning about this, but the wasted work didn't go anywhere.
// ✅ Guard with an isMounted flaguseEffect(() => { let isMounted = true; const fetchData = async () => { const result = await api.getItems(); if (isMounted) { setItems(result); } }; fetchData(); return () => { isMounted = false; };}, []);
AbortController cancels the request itself and is the more correct answer if you care about bandwidth. For patching Rork output, though, the isMounted flag produces a smaller diff and doesn't disturb the generated structure. "Protect the async work with an isMounted pattern" lands on the first try more often than an AbortController instruction does.
RevenueCat behaves the same way: anything registered with Purchases.addCustomerInfoUpdateListener needs a matching Purchases.removeCustomerInfoUpdateListener. An app of mine missing that removal climbed 4MB of heap over a month of use. Paywall screens open rarely, which is precisely why the round-trip test can miss them.
Pattern 5: FlashList Key Collisions, and What Changed in v2
Not strictly a leak, but it surfaces as a heap spike on list screens. FlashList recycles cells internally, so a keyExtractor returning non-unique values causes duplicate cells to be retained.
// ❌ Duplicate IDs pile up in the heap<FlashList data={items} keyExtractor={(item) => item.userId} renderItem={renderItem}/>// ✅ Guarantee uniqueness<FlashList data={items} keyExtractor={(item) => `${item.userId}-${item.timestamp}`} renderItem={renderItem}/>
One caution when following older articles: FlashList v2 is a ground-up rewrite for the New Architecture, and estimatedItemSize, estimatedListSize, and estimatedFirstItemOffset are no longer needed. Synchronous layout measurement removed the need for human-supplied estimates. Copying estimatedItemSize out of a v1 article only adds a deprecated prop. If Rork's output still carries these, strip them when you move to v2.
Prop
v1
v2
estimatedItemSize
Required, close to actual
Not needed (deprecated)
keyExtractor
Must be unique
Must be unique (unchanged)
New Architecture
Optional
Required
Going Deeper with Xcode Instruments (iOS)
DevTools shows you the JS heap. Native allocations — leaked UIView instances, image buffers — never appear there. When the JS heap returns to baseline but total memory keeps climbing on iOS, Instruments is the next step.
Step 1: Launch and Configure
# With Rork Max you have a real Xcode project, so you can profile directly# Open the .xcworkspace in Xcode# Product → Profile (Cmd + I) → choose the Leaks template# Once running, use the "+" at the bottom left to add the Allocations template
Adding Allocations lets you track objects that are still referenced but never released — the "abandoned memory" tier that Leaks won't flag despite causing real harm. Checking Leaks alone and calling it clean misses this layer entirely.
Step 2: The Protocol
Run the same ten-round-trip test in Instruments, pressing "Mark Generation" (the flag icon) after each round-trip. That lets you track newly allocated memory generation by generation.
If a specific class keeps growing across generations, that's your source. On one app, UIView subclasses grew by three to five per round-trip; the cause was a missing removeFromSuperview. No amount of reading the JS made it visible — it only appeared once Instruments was open.
Step 3: Interpreting the Results
Instruments has a steeper learning curve, but it shows exactly which objects aren't being released and what's referencing them. Keep it as your second move for when DevTools confirms a leak exists but won't tell you why.
Verifying the Fix
After making changes, confirm the improvement like this:
Launch the app with DevTools attached
Run the leak regression protocol (snapshot → 10 round-trips → snapshot)
In the Comparison view, confirm the suspect class's # Delta has dropped to near zero
Confirm total heap growth is under 3MB
On iOS, run the Instruments Generations check
Before submission, do a 30-minute continuous-use test on a physical device
Don't skip step three. Watching only total heap growth means a newly introduced leak can offset a fix you just made, and the number tells you nothing was wrong. Only when the specific class's delta has individually dropped can you call it fixed.
A Prompt Template for Rork
If editing the code yourself isn't practical, hand it back to Rork. Here's the prompt I use:
Review the following file and fix every source of memory leaks:
- Do useEffect event listeners have cleanup functions?
- Are setInterval / setTimeout calls cleared?
- Are requestAnimationFrame loops cancelled?
- Are Supabase / Firebase realtime subscriptions removed?
- Is RevenueCat's addCustomerInfoUpdateListener removed?
- Does FlashList keyExtractor return unique values?
- Does any async work write state after unmount?
Constraints:
- Do not modify existing dependency arrays
- Prefer the isMounted flag pattern
- List each change and the reason for it afterwards
Pairing this prompt with one specific file, like src/screens/YourScreen.tsx, gets accurate results. Passing several files at once increases the chance of missed spots, so I go one file at a time.
Impact on eCPM and Retention
Leaving leaks in place affects revenue, not just technical hygiene. Here's the before/after from fixing leaks on the wallpaper app:
Metric
Before
After
Change
D1 retention
38%
41%
+3pt
D7 retention
11%
13%
+2pt
Average session length
2m10s
2m45s
+27%
AdMob eCPM
$4.20
$5.15
+22%
I can't cleanly isolate causation here. Other improvements shipped in the same window, and seasonality plays a part. Still, the direction was consistent: longer sessions meant more ad impressions, and lower bounce meant stronger bidding. AdMob tends to pay less for apps where "an ad was shown but no further session followed," so retention decline and eCPM decline travel together. Reframing leak fixes as revenue work, rather than cleanup work, changed how I prioritize them.
Building the Habit
Preventing leaks is much cheaper than chasing them. What I check every time Rork generates code:
Every useEffect gets the question: does this need a cleanup function?
Every timer (setInterval, setTimeout, requestAnimationFrame) gets a clear or cancel attached by reflex
Every external subscription (Supabase, Firebase, WebSocket, RevenueCat) gets an explicit teardown
FlashList keyExtractor uses compound keys for guaranteed uniqueness
After changing a key screen, run the two-snapshot comparison in DevTools
Insert one Instruments Generations check before store submission
The value of having a diagnostic procedure isn't really that you catch leaks. It's that a vague worry — "this feels like it's getting slower" — becomes a number you can answer. Once there's an answer, deciding whether to fix now or defer gets a lot calmer.
Start by searching your codebase for useEffect and checking each one for a missing cleanup. There's a good chance one or two turn up 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.