Memory Management
Prevent memory leaks and optimize memory usage in React applications
Memory issues cause performance degradation over time, leading to sluggish interactions and crashes. Understanding JavaScript and React memory patterns is essential for building performant applications.
Common Memory Leaks
Uncleared Intervals and Timeouts
// ❌ Memory leak: interval never cleared
function Component() {
useEffect(() => {
setInterval(() => {
updateData();
}, 1000);
}, []);
}
// ✅ Fixed: Clear interval on cleanup
function Component() {
useEffect(() => {
const intervalId = setInterval(() => {
updateData();
}, 1000);
return () => clearInterval(intervalId);
}, []);
}Event Listeners
// ❌ Memory leak: listener never removed
function Component() {
useEffect(() => {
window.addEventListener('resize', handleResize);
}, []);
}
// ✅ Fixed: Remove listener on cleanup
function Component() {
useEffect(() => {
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
}Subscriptions
// ❌ Memory leak: subscription never unsubscribed
function Component() {
useEffect(() => {
const subscription = dataSource.subscribe(data => {
setData(data);
});
}, []);
}
// ✅ Fixed: Unsubscribe on cleanup
function Component() {
useEffect(() => {
const subscription = dataSource.subscribe(data => {
setData(data);
});
return () => subscription.unsubscribe();
}, []);
}Abort Controllers
// ❌ Memory leak: fetch continues after unmount
function Component({ id }) {
useEffect(() => {
fetch(`/api/data/${id}`)
.then(r => r.json())
.then(data => setData(data));
}, [id]);
}
// ✅ Fixed: Abort fetch on cleanup
function Component({ id }) {
useEffect(() => {
const controller = new AbortController();
fetch(`/api/data/${id}`, { signal: controller.signal })
.then(r => r.json())
.then(data => setData(data))
.catch(e => {
if (e.name !== 'AbortError') throw e;
});
return () => controller.abort();
}, [id]);
}Closures and References
Stale Closures
// ❌ Problem: Stale closure captures old value
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // Always uses initial count (0)
}, 1000);
return () => clearInterval(id);
}, []); // Missing count dependency
}
// ✅ Fixed: Use functional update
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1); // Uses current value
}, 1000);
return () => clearInterval(id);
}, []);
}Callback References
// ❌ Problem: Callback keeps reference to old data
function List({ items }) {
const [selected, setSelected] = useState(null);
const handleSelect = (id) => {
const item = items.find(i => i.id === id); // Captures items
setSelected(item);
};
return items.map(item => (
<Item key={item.id} onClick={() => handleSelect(item.id)} />
));
}
// ✅ Fixed: Pass item directly or use ref
function List({ items }) {
const itemsRef = useRef(items);
itemsRef.current = items;
const handleSelect = useCallback((id) => {
const item = itemsRef.current.find(i => i.id === id);
setSelected(item);
}, []);
}Large Data Handling
Virtualization
// ❌ Bad: All items in memory and DOM
function HugeList({ items }) {
return items.map(item => <ListItem key={item.id} {...item} />);
}
// ✅ Good: Only visible items in DOM
import { useVirtualizer } from '@tanstack/react-virtual';
function HugeList({ items }) {
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
});
return (
<div ref={parentRef}>
{virtualizer.getVirtualItems().map(row => (
<ListItem key={row.key} {...items[row.index]} />
))}
</div>
);
}Pagination
// Load data in pages instead of all at once
function PaginatedList() {
const [page, setPage] = useState(1);
const { data, isLoading } = useQuery({
queryKey: ['items', page],
queryFn: () => fetchPage(page),
keepPreviousData: true,
});
return (
<div>
{data?.items.map(item => <Item key={item.id} {...item} />)}
<Pagination page={page} onPageChange={setPage} />
</div>
);
}Clearing Unused Data
// Clear data when navigating away
function ProductPage({ productId }) {
const [product, setProduct] = useState(null);
const [reviews, setReviews] = useState([]);
useEffect(() => {
loadProduct(productId).then(setProduct);
loadReviews(productId).then(setReviews);
return () => {
// Clear heavy data on unmount
setProduct(null);
setReviews([]);
};
}, [productId]);
}WeakMap and WeakRef
WeakMap for Caching
// Cache that allows garbage collection
const cache = new WeakMap<object, ProcessedData>();
function processObject(obj: object): ProcessedData {
if (cache.has(obj)) {
return cache.get(obj)!;
}
const result = expensiveProcessing(obj);
cache.set(obj, result);
return result;
}WeakRef for Optional References
class Cache {
private cache = new Map<string, WeakRef<object>>();
set(key: string, value: object) {
this.cache.set(key, new WeakRef(value));
}
get(key: string): object | undefined {
const ref = this.cache.get(key);
return ref?.deref();
}
}Profiling Memory
Chrome DevTools Memory Panel
Open DevTools → Memory tab
Select "Heap snapshot"
Take snapshot before the action
Perform the action you're investigating
Take snapshot after
Compare snapshots using the dropdown to find retained objects
Programmatic Memory Monitoring
// Monitor memory usage
function logMemory() {
if ('memory' in performance) {
const memory = (performance as any).memory;
console.log({
usedJSHeapSize: formatBytes(memory.usedJSHeapSize),
totalJSHeapSize: formatBytes(memory.totalJSHeapSize),
jsHeapSizeLimit: formatBytes(memory.jsHeapSizeLimit),
});
}
}
function formatBytes(bytes: number): string {
return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
}React-Specific Patterns
useRef for Non-Reactive Data
// Store data that doesn't need re-renders
function Component() {
const cache = useRef(new Map());
const lastValue = useRef(null);
// These don't cause re-renders
cache.current.set('key', 'value');
lastValue.current = computedValue;
}Cleanup in Custom Hooks
function useWebSocket(url: string) {
const [data, setData] = useState(null);
const wsRef = useRef<WebSocket | null>(null);
useEffect(() => {
wsRef.current = new WebSocket(url);
wsRef.current.onmessage = (event) => {
setData(JSON.parse(event.data));
};
return () => {
wsRef.current?.close();
wsRef.current = null;
};
}, [url]);
return data;
}Checklist
Effect Cleanup
Clear intervals and timeouts Store IDs and call clearInterval/clearTimeout in cleanup.
Remove event listeners Always remove listeners added to window/document.
Unsubscribe from subscriptions Call unsubscribe on any observable or event emitter.
Abort pending fetches Use AbortController to cancel in-flight requests.
Close WebSocket connections Call close() on cleanup to prevent orphaned connections.
Data Management
Virtualize long lists Only render visible items to reduce DOM size.
Paginate large datasets Load data in chunks instead of all at once.
Clear data on navigation Set state to null when unmounting data-heavy components.
Use WeakMap for caches Allows garbage collection of cached objects.
References
Avoid stale closures Use functional updates or refs for current values.
Use refs for mutable values Store non-reactive data in useRef to avoid re-renders.
Clear refs on cleanup Set ref.current to null in cleanup functions.
Use WeakRef when appropriate For optional references that shouldn't prevent GC.
Monitoring
Profile with DevTools Use Chrome's Memory panel to identify leaks.
Take heap snapshots Compare before/after snapshots to find retained objects.
Compare memory over time Watch for gradual increases during usage.
Test on memory-constrained devices Mobile devices expose memory issues faster.
For advanced memory forensics with heap snapshots and retainer trees, see DevTools Debugging.