Memory Management

Garbage collection, memory leaks, WeakMap, WeakSet, and performance optimization

Understanding Memory in JavaScript

JavaScript automatically manages memory through garbage collection, but understanding how it works helps you write more efficient code and avoid memory leaks. Poor memory management can lead to slow applications and crashes.

Memory Lifecycle

  1. Allocation — Memory is allocated when you create values
  2. Usage — Memory is used when you read/write values
  3. Release — Memory is freed when no longer needed (GC)

Garbage Collection Basics

// Memory is allocated
let user = { name: "Alice" };
let reference = user;

// Object is still reachable via 'reference'
user = null;

// Now no references exist - GC can collect
reference = null;

// The object { name: "Alice" } will be garbage collected

// Mark-and-sweep algorithm:
// 1. Start from "roots" (global object, current call stack)
// 2. Mark all reachable objects
// 3. Sweep (delete) unmarked objects

Common Memory Leaks

1. Accidental Global Variables

// ❌ BAD: Accidental global (missing 'let' or 'const')
function createUser() {
  user = { name: "Bob" }; // Attaches to window!
}

// ✅ GOOD: Use strict mode and proper declarations
"use strict";
function createUser() {
  const user = { name: "Bob" };
  return user;
}

2. Forgotten Event Listeners

// ❌ BAD: Listeners not removed
function setupHandler() {
  const button = document.getElementById("btn");
  button.addEventListener("click", handleClick);
}

// Later, if button is removed from DOM but listener remains...
// Memory leak!

// ✅ GOOD: Clean up listeners
class Component {
  constructor() {
    this.handleClick = this.handleClick.bind(this);
  }
  
  mount() {
    document.getElementById("btn").addEventListener("click", this.handleClick);
  }
  
  unmount() {
    document.getElementById("btn").removeEventListener("click", this.handleClick);
  }
}

// ✅ BETTER: Use AbortController
const controller = new AbortController();

button.addEventListener("click", handleClick, { signal: controller.signal });

// Clean up all listeners at once
controller.abort();

3. Closures Holding References

// ❌ BAD: Closure retains large data
function processData() {
  const hugeData = new Array(1000000).fill("x");
  
  return function() {
    // Closure keeps hugeData in memory forever
    return hugeData.length;
  };
}

const getLength = processData();
// hugeData stays in memory!

// ✅ GOOD: Only capture what you need
function processData() {
  const hugeData = new Array(1000000).fill("x");
  const length = hugeData.length; // Extract needed value
  
  return function() {
    return length; // Only captures the number
  };
}

4. Forgotten Timers

// ❌ BAD: Timer keeps running after component is gone
function startPolling() {
  setInterval(() => {
    fetch("/api/data").then(updateUI);
  }, 1000);
}

// ✅ GOOD: Store and clear timers
let pollingId;

function startPolling() {
  pollingId = setInterval(() => {
    fetch("/api/data").then(updateUI);
  }, 1000);
}

function stopPolling() {
  clearInterval(pollingId);
}

WeakMap and WeakSet

Weak references don't prevent garbage collection:

// Regular Map - prevents GC
const cache = new Map();
let user = { id: 1, name: "Alice" };
cache.set(user, "cached data");

user = null;
// Object still exists in cache - memory leak!
console.log(cache.size); // 1

// WeakMap - allows GC
const weakCache = new WeakMap();
let user2 = { id: 2, name: "Bob" };
weakCache.set(user2, "cached data");

user2 = null;
// Object can be garbage collected!
// weakCache entry automatically removed

// WeakMap use cases:
// 1. Private data for objects
const privateData = new WeakMap();

class User {
  constructor(name, password) {
    privateData.set(this, { password });
    this.name = name;
  }
  
  checkPassword(pwd) {
    return privateData.get(this).password === pwd;
  }
}

// 2. Caching computed values
const computed = new WeakMap();

function getExpensiveValue(obj) {
  if (!computed.has(obj)) {
    computed.set(obj, expensiveComputation(obj));
  }
  return computed.get(obj);
}
// WeakSet - track objects without preventing GC
const visited = new WeakSet();

function processOnce(obj) {
  if (visited.has(obj)) {
    return; // Already processed
  }
  
  visited.add(obj);
  // Process object...
}

// When obj is no longer referenced elsewhere,
// it's removed from visited automatically

WeakRef and FinalizationRegistry

// WeakRef - hold weak reference to object
let obj = { data: "important" };
const weakRef = new WeakRef(obj);

// Later, check if still available
const maybeObj = weakRef.deref();
if (maybeObj) {
  console.log(maybeObj.data);
} else {
  console.log("Object was garbage collected");
}

// FinalizationRegistry - callback when object is collected
const registry = new FinalizationRegistry((heldValue) => {
  console.log(`Object with ID ${heldValue} was collected`);
  // Clean up external resources
});

let resource = { id: 123 };
registry.register(resource, resource.id);

resource = null;
// Eventually logs: "Object with ID 123 was collected"

Memory Profiling

// Chrome DevTools Memory tab:
// 1. Take heap snapshot
// 2. Record allocations
// 3. Compare snapshots to find leaks

// Performance API
console.log(performance.memory); // Chrome only
// {
//   usedJSHeapSize: 10000000,
//   totalJSHeapSize: 35000000,
//   jsHeapSizeLimit: 2197815296
// }

// Manual memory tracking
function getMemoryUsage() {
  if (performance.memory) {
    return {
      used: Math.round(performance.memory.usedJSHeapSize / 1048576) + " MB",
      total: Math.round(performance.memory.totalJSHeapSize / 1048576) + " MB"
    };
  }
  return "Not available";
}

Best Practices

// 1. Nullify references when done
let largeData = loadData();
processData(largeData);
largeData = null; // Allow GC

// 2. Use object pools for frequent allocations
class ObjectPool {
  constructor(createFn) {
    this.pool = [];
    this.create = createFn;
  }
  
  acquire() {
    return this.pool.pop() || this.create();
  }
  
  release(obj) {
    this.pool.push(obj);
  }
}

// 3. Avoid creating objects in loops
// ❌ BAD
for (let i = 0; i < 1000; i++) {
  processPoint({ x: i, y: i * 2 });
}

// ✅ GOOD - reuse object
const point = { x: 0, y: 0 };
for (let i = 0; i < 1000; i++) {
  point.x = i;
  point.y = i * 2;
  processPoint(point);
}

// 4. Use typed arrays for large numeric data
const floats = new Float32Array(1000000); // More efficient

💡 Key Takeaways

  • • JavaScript uses mark-and-sweep garbage collection
  • • Common leaks: globals, event listeners, closures, timers
  • • Use WeakMap/WeakSet for caches and metadata
  • • Clean up listeners and timers when done
  • • Use Chrome DevTools Memory tab to find leaks
  • • Consider object pools for performance-critical code