Back to patterns
Memory Leaks 101
PERFORMANCE
A systematic approach to identifying and fixing memory leaks through heap snapshot analysis, memory usage visualization, and common leak pattern detection.
Use Cases
- Long-running web applications
- Single-page applications (SPAs)
- Real-time data processing systems
- Applications with dynamic component loading
Production Implementation
Implementation
// Real-world example: Memory leak detection and prevention in a React-like application
interface Subscription {
unsubscribe(): void;
}
interface CacheOptions {
maxSize?: number;
ttl?: number;
}
// Memory-safe cache implementation with size and TTL limits
class LRUCache<K, V> {
private cache: Map<K, { value: V; timestamp: number }>;
private maxSize: number;
private ttl: number;
constructor(options: CacheOptions = {}) {
this.cache = new Map();
this.maxSize = options.maxSize || 1000;
this.ttl = options.ttl || 3600000; // 1 hour default
// Periodic cleanup to prevent memory leaks
setInterval(() => this.cleanup(), 60000);
}
set(key: K, value: V): void {
// Enforce size limit
if (this.cache.size >= this.maxSize) {
const oldestKey = this.cache.keys().next().value;
this.cache.delete(oldestKey);
}
this.cache.set(key, {
value,
timestamp: Date.now()
});
}
get(key: K): V | undefined {
const item = this.cache.get(key);
if (!item) return undefined;
// Check TTL
if (Date.now() - item.timestamp > this.ttl) {
this.cache.delete(key);
return undefined;
}
return item.value;
}
private cleanup(): void {
const now = Date.now();
for (const [key, item] of this.cache.entries()) {
if (now - item.timestamp > this.ttl) {
this.cache.delete(key);
}
}
}
}
// Memory-safe event emitter with automatic cleanup
class EventEmitter {
private events: Map<string, Set<WeakRef<Function>>>;
private cleanupInterval: number;
constructor() {
this.events = new Map();
// Periodic cleanup of dead references
this.cleanupInterval = setInterval(() => this.cleanup(), 30000);
}
on(event: string, callback: Function): Subscription {
if (!this.events.has(event)) {
this.events.set(event, new Set());
}
const callbacks = this.events.get(event)!;
const weakCallback = new WeakRef(callback);
callbacks.add(weakCallback);
return {
unsubscribe: () => {
callbacks.delete(weakCallback);
if (callbacks.size === 0) {
this.events.delete(event);
}
}
};
}
emit(event: string, ...args: any[]): void {
const callbacks = this.events.get(event);
if (!callbacks) return;
for (const weakCallback of callbacks) {
const callback = weakCallback.deref();
if (callback) {
callback(...args);
} else {
callbacks.delete(weakCallback);
}
}
}
private cleanup(): void {
for (const [event, callbacks] of this.events.entries()) {
for (const weakCallback of callbacks) {
if (!weakCallback.deref()) {
callbacks.delete(weakCallback);
}
}
if (callbacks.size === 0) {
this.events.delete(event);
}
}
}
destroy(): void {
clearInterval(this.cleanupInterval);
this.events.clear();
}
}
// Memory-safe component base class
abstract class Component {
private subscriptions: Set<Subscription>;
private timers: Set<number>;
private eventEmitter: EventEmitter;
private mounted: boolean;
constructor() {
this.subscriptions = new Set();
this.timers = new Set();
this.eventEmitter = new EventEmitter();
this.mounted = false;
}
protected onMount(): void {
this.mounted = true;
}
protected onUnmount(): void {
this.mounted = false;
this.cleanup();
}
// Safe subscription management
protected subscribe(subscription: Subscription): void {
this.subscriptions.add(subscription);
}
// Safe timer management
protected setTimeout(callback: () => void, delay: number): number {
const id = window.setTimeout(() => {
if (this.mounted) {
callback();
}
this.timers.delete(id);
}, delay);
this.timers.add(id);
return id;
}
protected setInterval(callback: () => void, delay: number): number {
const id = window.setInterval(() => {
if (!this.mounted) {
clearInterval(id);
this.timers.delete(id);
return;
}
callback();
}, delay);
this.timers.add(id);
return id;
}
// Safe event listener management
protected addEventListener(
target: EventTarget,
type: string,
listener: EventListener,
options?: AddEventListenerOptions
): void {
target.addEventListener(type, listener, options);
this.subscriptions.add({
unsubscribe: () => target.removeEventListener(type, listener)
});
}
private cleanup(): void {
// Clear subscriptions
for (const subscription of this.subscriptions) {
subscription.unsubscribe();
}
this.subscriptions.clear();
// Clear timers
for (const timer of this.timers) {
clearTimeout(timer);
}
this.timers.clear();
// Cleanup event emitter
this.eventEmitter.destroy();
}
}
// Example usage
class DataGrid extends Component {
private cache: LRUCache<string, any>;
constructor() {
super();
this.cache = new LRUCache({ maxSize: 100, ttl: 300000 }); // 5 minutes TTL
}
protected onMount(): void {
super.onMount();
// Safe event listener
this.addEventListener(window, 'resize', this.handleResize);
// Safe interval
this.setInterval(this.refreshData, 60000);
// Safe subscription
const subscription = dataService.subscribe(
this.handleDataUpdate
);
this.subscribe(subscription);
}
private handleResize = () => {
// Handle resize logic
};
private refreshData = () => {
// Refresh data logic
};
private handleDataUpdate = (data: any) => {
this.cache.set('latestData', data);
};
}
Code Examples
Common Memory Leak Patterns
typescript
// ❌ Common memory leak patterns
class DataComponent {
private eventHandlers = new Set();
private cache = {};
constructor() {
// Issue 1: Unbounded cache growth
this.cache = {};
// Issue 2: Event listener not removed
window.addEventListener('resize', this.handleResize);
// Issue 3: Interval not cleared
setInterval(this.fetchData, 5000);
// Issue 4: Closure keeping reference
const data = { value: 'test' };
this.eventHandlers.add(() => {
console.log(data.value);
});
}
handleResize = () => {
// Handle resize
};
fetchData = () => {
// Fetch and cache data
this.cache[Date.now()] = 'data';
};
}
Common memory leaks include unbounded caches, uncleared intervals, uncleaned event listeners, and closure references.
Memory-Safe Implementation
typescript
// ✅ Memory-safe implementation
class DataComponent {
private eventHandlers = new Set<() => void>();
private cache = new LRUCache<string, any>({ maxSize: 100 });
private intervalId?: number;
constructor() {
// Bounded LRU cache
this.cache = new LRUCache({ maxSize: 100, ttl: 3600000 });
// Tracked event listener
this.addEventHandler(
'resize',
this.handleResize
);
// Tracked interval
this.intervalId = window.setInterval(this.fetchData, 5000);
}
private addEventHandler(
event: string,
handler: EventListener
): void {
window.addEventListener(event, handler);
this.eventHandlers.add(
() => window.removeEventListener(event, handler)
);
}
handleResize = () => {
// Handle resize
};
fetchData = () => {
// Cache with TTL
this.cache.set(Date.now().toString(), 'data');
};
destroy(): void {
// Clean up event listeners
for (const cleanup of this.eventHandlers) {
cleanup();
}
this.eventHandlers.clear();
// Clear interval
if (this.intervalId) {
clearInterval(this.intervalId);
}
// Clear cache
this.cache.clear();
}
}
This implementation uses bounded caches, tracks and cleans up event listeners, and properly manages intervals.
Best Practices
- Use WeakMap/WeakSet for caching object references
- Implement size limits and TTL for caches
- Track and clean up all event listeners
- Clear intervals and timeouts on component destruction
- Use weak references for event callbacks
- Implement proper cleanup methods
- Monitor memory usage in development
Common Pitfalls
- Unbounded caches leading to memory growth
- Uncleaned event listeners in SPAs
- Forgotten setInterval cleanup
- Circular references in data structures
- Closure variables keeping references alive
- Not implementing proper cleanup methods