Skip to main content
Technology & EngineeringPerformance Optimization173 lines

Memory Management

Prevent memory leaks and optimize memory usage through proper cleanup patterns, profiling, and garbage collection awareness.

Quick Summary18 lines
You are an expert in memory leak prevention and management for optimizing application performance.

## Key Points

- **Garbage collection (GC):** JavaScript engines use mark-and-sweep GC — objects unreachable from root references are collected. Leaks occur when unintended references keep objects reachable.
- **Heap snapshots:** A snapshot of all objects in memory at a point in time. Comparing two snapshots reveals objects that were allocated but never released.
- **Detached DOM nodes:** DOM elements removed from the document tree but still referenced in JavaScript. A common source of leaks in single-page applications.
- **Closures retaining scope:** A closure captures its enclosing scope. If a long-lived closure references a large object, that object cannot be garbage collected.
- **WeakRef / WeakMap:** References that do not prevent garbage collection. Useful for caches and observer patterns where you want automatic cleanup.
- **Chrome DevTools Memory tab:** Take heap snapshots before and after a user workflow. The "Comparison" view highlights objects allocated between snapshots that were not released.
- **Performance Monitor (DevTools):** Real-time view of JS heap size, DOM node count, and event listener count. A steadily rising heap graph signals a leak.
- **Node.js `--inspect`:** Connect Chrome DevTools to a running Node process to take heap snapshots and profiles remotely.
- **Allocation timeline:** Records every allocation over time. Bars that remain (not collected) indicate potential leaks. Filter by constructor name to find the leaking object type.
- Always return cleanup functions from `useEffect` in React — clear intervals, remove listeners, abort fetches, and close connections.
- Use `WeakMap` and `WeakRef` for caches and observer registries to allow automatic garbage collection of unused entries.
- Set a memory budget for your application (e.g., < 100MB heap for a frontend SPA) and alert when it is exceeded in production monitoring.
skilldb get performance-optimization-skills/Memory ManagementFull skill: 173 lines
Paste into your CLAUDE.md or agent config

Memory Management — Performance Optimization

You are an expert in memory leak prevention and management for optimizing application performance.

Core Philosophy

Overview

Memory leaks occur when an application retains references to objects that are no longer needed, preventing the garbage collector from reclaiming that memory. Over time, leaks cause increasing memory consumption, degraded performance, and eventually application crashes. Effective memory management requires understanding reference lifecycles, using profiling tools, and applying disciplined cleanup patterns.

Core Concepts

  • Garbage collection (GC): JavaScript engines use mark-and-sweep GC — objects unreachable from root references are collected. Leaks occur when unintended references keep objects reachable.
  • Heap snapshots: A snapshot of all objects in memory at a point in time. Comparing two snapshots reveals objects that were allocated but never released.
  • Detached DOM nodes: DOM elements removed from the document tree but still referenced in JavaScript. A common source of leaks in single-page applications.
  • Closures retaining scope: A closure captures its enclosing scope. If a long-lived closure references a large object, that object cannot be garbage collected.
  • WeakRef / WeakMap: References that do not prevent garbage collection. Useful for caches and observer patterns where you want automatic cleanup.

Implementation Patterns

Clean Up Event Listeners

Before (leaks on component unmount):

function ChatWidget() {
  useEffect(() => {
    const handler = (e) => handleMessage(e.data);
    window.addEventListener('message', handler);
    // Missing cleanup — listener persists after unmount
  }, []);
}

After:

function ChatWidget() {
  useEffect(() => {
    const handler = (e) => handleMessage(e.data);
    window.addEventListener('message', handler);
    return () => window.removeEventListener('message', handler);
  }, []);
}

Clear Timers and Subscriptions

Before:

function PollingComponent({ url }) {
  useEffect(() => {
    const id = setInterval(() => fetch(url), 5000);
    // Interval runs forever, even after unmount
  }, [url]);
}

After:

function PollingComponent({ url }) {
  useEffect(() => {
    const controller = new AbortController();
    const id = setInterval(() => {
      fetch(url, { signal: controller.signal });
    }, 5000);

    return () => {
      clearInterval(id);
      controller.abort();
    };
  }, [url]);
}

Avoid Detached DOM References

Before:

const elements = [];

function createListItem(text) {
  const li = document.createElement('li');
  li.textContent = text;
  elements.push(li); // retains reference even after removal from DOM
  list.appendChild(li);
}

function clearList() {
  list.innerHTML = '';
  // elements array still holds references to detached nodes
}

After:

function createListItem(text) {
  const li = document.createElement('li');
  li.textContent = text;
  list.appendChild(li);
  // No external array — let the DOM be the single source of truth
}

function clearList() {
  list.innerHTML = '';
  // All references released
}

Use WeakMap for Metadata Caches

// Strong map — leaks if objects are discarded elsewhere
const metadata = new Map();

// WeakMap — entries are automatically collected when keys are GC'd
const metadata = new WeakMap();

function annotate(obj, data) {
  metadata.set(obj, data);
  // When obj is no longer referenced elsewhere, both the key and value are collected
}

Node.js — Monitor Process Memory

function logMemory() {
  const { heapUsed, heapTotal, rss } = process.memoryUsage();
  console.log({
    heapUsed: `${(heapUsed / 1024 / 1024).toFixed(1)} MB`,
    heapTotal: `${(heapTotal / 1024 / 1024).toFixed(1)} MB`,
    rss: `${(rss / 1024 / 1024).toFixed(1)} MB`,
  });
}

// Log every 30 seconds to detect trends
setInterval(logMemory, 30000);

Measurement & Monitoring

  • Chrome DevTools Memory tab: Take heap snapshots before and after a user workflow. The "Comparison" view highlights objects allocated between snapshots that were not released.
  • Performance Monitor (DevTools): Real-time view of JS heap size, DOM node count, and event listener count. A steadily rising heap graph signals a leak.
  • Node.js --inspect: Connect Chrome DevTools to a running Node process to take heap snapshots and profiles remotely.
  • Allocation timeline: Records every allocation over time. Bars that remain (not collected) indicate potential leaks. Filter by constructor name to find the leaking object type.

Best Practices

  • Always return cleanup functions from useEffect in React — clear intervals, remove listeners, abort fetches, and close connections.
  • Use WeakMap and WeakRef for caches and observer registries to allow automatic garbage collection of unused entries.
  • Set a memory budget for your application (e.g., < 100MB heap for a frontend SPA) and alert when it is exceeded in production monitoring.

Common Pitfalls

  • Storing references to DOM nodes or component instances in module-level variables or singletons, which survive across route navigations in SPAs and accumulate over time.
  • Growing in-memory caches without an eviction policy (LRU, TTL, or max-size cap), causing unbounded memory growth proportional to usage duration.

Anti-Patterns

Over-engineering for hypothetical scale. Building for millions of users when you have hundreds adds complexity without value. Solve today's problems first.

Ignoring the existing ecosystem. Reinventing functionality that mature libraries already provide well wastes time and introduces unnecessary risk.

Premature abstraction. Creating elaborate frameworks and utilities before you have enough concrete cases to know what the abstraction should look like produces the wrong abstraction.

Neglecting error handling at boundaries. Internal code can trust its inputs, but system boundaries (user input, APIs, file I/O) require defensive validation.

Skipping documentation for obvious code. What is obvious to you today will not be obvious to your colleague next month or to you next year.

Install this skill directly: skilldb add performance-optimization-skills

Get CLI access →