Memory Management & Prototype System

📖 Concept

Memory Management in JavaScript uses automatic garbage collection (GC). The engine (V8, Hermes, JSC) allocates memory when objects are created and frees it when they're no longer reachable. But "automatic" doesn't mean "worry-free" — memory leaks are one of the top production issues in React Native.

Memory lifecycle:

  1. Allocation — When you declare variables, create objects, or call functions
  2. Usage — Reading/writing to allocated memory
  3. Deallocation — GC removes unreachable objects (no references pointing to them)

GC algorithms used by JS engines:

  • Mark-and-Sweep (V8/Hermes primary): Starts from roots (global, stack), marks all reachable objects, sweeps unmarked ones
  • Generational GC (V8): Young generation (nursery) for short-lived objects, old generation for long-lived. Most objects die young — optimize for that
  • Incremental/Concurrent GC: GC runs in small slices to avoid long pauses

React Native Memory Leak Patterns:

  1. Unmounted component listeners — Event listeners not cleaned up in useEffect return
  2. Closure references — Callbacks holding references to unmounted component state
  3. Global caches — Unbounded caches that grow without eviction
  4. Circular references — Objects referencing each other preventing collection
  5. Native module leaks — Native allocations not released when JS objects are GC'd

The Prototype System: Every JavaScript object has an internal [[Prototype]] link forming a chain. Property lookups traverse this chain. Understanding prototypes is essential because:

  • React Native components extend React.Component through the prototype chain
  • Performance: prototype method lookup is O(chain length)
  • class syntax is syntactic sugar over prototypes
  • Understanding this binding requires understanding prototype dispatch

💻 Code Example

codeTap to expand ⛶
1// === MEMORY LEAK PATTERNS IN REACT NATIVE ===
2
3// ❌ LEAK: Event listener not cleaned up
4function ChatScreen() {
5 const [messages, setMessages] = useState([]);
6
7 useEffect(() => {
8 const subscription = EventEmitter.addListener('newMessage', (msg) => {
9 setMessages(prev => [...prev, msg]);
10 });
11 // Missing: return () => subscription.remove();
12 // When this screen unmounts, the listener persists,
13 // holding a reference to setMessages → component → entire subtree
14 }, []);
15}
16
17// ✅ FIX: Always return cleanup
18useEffect(() => {
19 const subscription = EventEmitter.addListener('newMessage', handler);
20 return () => subscription.remove();
21}, []);
22
23// ❌ LEAK: Unbounded cache
24const imageCache = new Map(); // Grows forever!
25function cacheImage(url, bitmap) {
26 imageCache.set(url, bitmap); // 50MB+ bitmaps accumulate
27}
28
29// ✅ FIX: LRU cache with max size
30class LRUCache<K, V> {
31 private cache = new Map<K, V>();
32 constructor(private maxSize: number) {}
33
34 get(key: K): V | undefined {
35 const value = this.cache.get(key);
36 if (value !== undefined) {
37 // Move to end (most recently used)
38 this.cache.delete(key);
39 this.cache.set(key, value);
40 }
41 return value;
42 }
43
44 set(key: K, value: V): void {
45 this.cache.delete(key); // Remove if exists (for reorder)
46 this.cache.set(key, value);
47 if (this.cache.size > this.maxSize) {
48 // Delete oldest (first) entry
49 const firstKey = this.cache.keys().next().value;
50 this.cache.delete(firstKey);
51 }
52 }
53}
54
55// ✅ WeakRef for optional caching (GC can collect if needed)
56const weakCache = new Map<string, WeakRef<object>>();
57function getCachedOrFetch(key: string): object | null {
58 const ref = weakCache.get(key);
59 const cached = ref?.deref();
60 if (cached) return cached;
61 // Fetch fresh data...
62 return null;
63}
64
65// === PROTOTYPE SYSTEM ===
66
67// How class inheritance actually works:
68class Animal {
69 constructor(name) { this.name = name; }
70 speak() { return `${this.name} makes a sound`; }
71}
72
73class Dog extends Animal {
74 speak() { return `${this.name} barks`; }
75}
76
77const dog = new Dog('Rex');
78// Prototype chain: dog → Dog.prototype → Animal.prototype → Object.prototype → null
79
80// Under the hood (what the engine does):
81// dog.__proto__ === Dog.prototype
82// Dog.prototype.__proto__ === Animal.prototype
83// dog.speak() → found on Dog.prototype (no traversal needed)
84// dog.toString() → not on dog, not on Dog.prototype,
85// found on Object.prototype (2 hops)
86
87// Performance implication: deep prototype chains = slower property lookups
88// In React Native, component hierarchies create deep chains:
89// MyComponent → React.Component → Object
90// Each render cycle traverses this chain for method resolution

🏋️ Practice Exercise

Memory & Prototype Exercises:

  1. Use Chrome DevTools (or Hermes profiler) to take heap snapshots before and after navigating to a screen and back — identify retained objects
  2. Implement an LRU cache with O(1) get/set using Map
  3. Create a memory leak deliberately (timer + closure) and then fix it
  4. Explain the prototype chain for: const arr = [1,2,3]; arr.map(...) — where does map come from?
  5. Implement Object.create from scratch to demonstrate prototype linkage
  6. Profile your React Native app's memory usage over 5 minutes of typical use — identify the growth rate

⚠️ Common Mistakes

  • Assuming garbage collection handles everything — closures, event listeners, and native references can prevent collection

  • Using global Maps/Sets as caches without eviction — they grow unbounded and crash low-memory devices

  • Not cleaning up useEffect subscriptions — the #1 memory leak cause in React Native apps

  • Storing large binary data (images, buffers) in JS heap — use native storage or streaming APIs instead

  • Confusing proto with prototype — proto is the internal link on instances, prototype is the object on constructors used to set proto

💼 Interview Questions

🎤 Mock Interview

Mock interview is powered by AI for Memory Management & Prototype System. Login to unlock this feature.