Skill: Hunt JS Memory Leaks
Find and fix JavaScript memory leaks using React Native DevTools memory profiling.
Quick Pattern
Incorrect (listener not cleaned up):
useEffect(() => {
const sub = EventEmitter.addListener('event', handler);
// Missing cleanup!
}, []);Correct (proper cleanup):
useEffect(() => {
const sub = EventEmitter.addListener('event', handler);
return () => sub.remove();
}, []);When to Use
- App memory usage grows over time
- App crashes after extended use
- Navigating between screens increases memory
- Suspecting event listeners or timers not cleaned up
Prerequisites
- React Native DevTools accessible
- App running in development mode
Step-by-Step Instructions
1. Open Memory Profiler
- Launch React Native DevTools (press
jin Metro) - Go to Memory tab
- Select "Allocation instrumentation on timeline"
2. Record Memory Allocations
- Click "Start" at the bottom
- Perform actions that might leak (navigate, trigger events, etc.)
- Wait 10-30 seconds
- Click "Stop"
3. Analyze the Timeline
Key indicators:
- Blue bars = Memory allocated
- Gray bars = Memory freed (garbage collected)
- Blue bars that stay blue = Potential leak!
4. Investigate Leaking Objects
The Memory tab shows:
- Timeline (top): Blue bars = allocations, select time range to filter
- Summary view (bottom): Lists constructors with allocation counts
Key columns:
- Constructor: Object type (e.g.,
JSObject,Function,(string)) - Count: Number of instances (×85000 = 85,000 objects)
- Shallow Size: Memory of the object itself
- Retained Size: Memory freed if object is deleted (including references)
Red flag: Large retained size % with small shallow size % = closures or references holding large objects.
To investigate:
- Click on a blue spike in the timeline
- Look at the Constructor list below
- Check Shallow size vs Retained size
- Expand constructors to see individual allocations
- Click to see the exact source location
5. Verify the Fix
After fixing, re-profile. All bars should turn gray (except the most recent).
Code Examples
Common Leak Patterns
1. Listeners Not Cleaned Up:
// BAD: Memory leak
const BadEventComponent = () => {
useEffect(() => {
const subscription = EventEmitter.addListener('myEvent', handleEvent);
// Missing cleanup!
}, []);
return <Text>Listening...</Text>;
};
// GOOD: Proper cleanup
const GoodEventComponent = () => {
useEffect(() => {
const subscription = EventEmitter.addListener('myEvent', handleEvent);
return () => subscription.remove(); // Cleanup!
}, []);
return <Text>Listening...</Text>;
};2. Timers Not Cleared:
// BAD: Memory leak
const BadTimerComponent = () => {
useEffect(() => {
const timer = setInterval(() => {
setCount(prev => prev + 1);
}, 1000);
// Missing cleanup!
}, []);
};
// GOOD: Proper cleanup
const GoodTimerComponent = () => {
useEffect(() => {
const timer = setInterval(() => {
setCount(prev => prev + 1);
}, 1000);
return () => clearInterval(timer); // Cleanup!
}, []);
};3. Closures Capturing Large Objects:
// BAD: Closure captures entire array
class BadClosureExample {
private largeData = new Array(1000000).fill('data');
createLeakyFunction() {
return () => this.largeData.length; // Captures this.largeData
}
}
// GOOD: Only capture what's needed
class GoodClosureExample {
private largeData = new Array(1000000).fill('data');
createEfficientFunction() {
const length = this.largeData.length; // Extract value
return () => length; // Only captures primitive
}
}4. Global Arrays Growing:
// BAD: Global array never cleared
let leakyClosures = [];
const createLeak = () => {
const data = generateLargeData();
leakyClosures.push(() => data); // Keeps growing!
};
// GOOD: Clear when done or use WeakRef
const createNoLeak = () => {
const data = generateLargeData();
const closure = () => data;
// Use it and let it be garbage collected
return closure;
};Memory Profiler Metrics
| Metric | Meaning |
|---|---|
| Shallow size | Memory held by the object itself |
| Retained size | Memory freed if object is deleted (includes references) |
Large retained size with small shallow size = Object holding references to other large objects (common in closures).
Common Pitfalls
- Not forcing GC: GC runs periodically. Allocate something else to trigger collection before concluding there's a leak.
- Ignoring gray bars: Gray = properly collected. Only blue bars that persist are leaks.
- Missing useEffect cleanup: Most common React Native leak source.
Related Skills
- native-memory-leaks.md - Native-side memory leaks
- js-profile-react.md - General profiling