Memory Basics

Course: JavaScript Advanced

Lesson 13: Memory Basics

Level

Advanced

Estimated Reading Time

10–15 minutes

Goal

By the end of this lesson, the learner will be able to:

  • Identify common sources of memory leaks in JavaScript applications
  • Use WeakMap and WeakSet to associate data with objects without preventing garbage collection
  • Clean up event listeners and timers correctly to prevent leaks in long-lived applications

Frontend Development Context

Memory leaks in frontend applications don't always crash the browser — often they cause gradual slowdowns that users notice after minutes or hours of use. Single-page applications (SPAs) are especially vulnerable because they never fully reload the page; JavaScript accumulates references across route changes, component mounts and unmounts, and event subscriptions. Understanding how JavaScript's garbage collector works and what prevents objects from being collected is essential for building applications that stay fast over time.

At the advanced level, memory management means knowing how closures can accidentally keep large objects alive, how event listeners on long-lived DOM nodes hold component closures in memory, and why WeakMap and WeakSet exist as GC-friendly alternatives to plain Map and Set for associating metadata with objects.

Explanation

JavaScript uses automatic garbage collection with a mark-and-sweep algorithm. The garbage collector (GC) periodically finds objects that are no longer reachable from any root (global variables, the call stack, active closures) and reclaims their memory. An object is kept alive as long as any reachable reference to it exists. A memory leak occurs when objects that should no longer be needed are kept alive by references that are never cleaned up.

Event listener leaks are one of the most common causes of memory leaks in SPAs. When a component adds an event listener to a DOM element or the global window/document and then the component's DOM is removed or replaced, the listener still holds a reference to the component's closure. The component cannot be garbage collected as long as that listener exists. The fix is to always call removeEventListener when the component is destroyed.

Timer leaks follow the same pattern. A setInterval callback holds a closure, and as long as the interval is running, that closure (and everything it references) is kept alive. If the component that set up the interval is removed from the page without clearing the interval, the timer and its references persist indefinitely.

Closures over large objects can cause unexpected retention. If a closure captures a reference to a large dataset — an array of thousands of items, a large canvas buffer — that entire dataset stays in memory as long as the closure exists. This happens subtly when event handlers or setTimeout callbacks reference outer-scope variables that hold large values.

WeakMap and WeakSet hold weak references to their keys (for WeakMap) or values (for WeakSet). A weak reference does not count for the purposes of garbage collection — if the only reference to an object is through a WeakMap key, the GC can still collect that object. The WeakMap entry is then automatically removed. This makes WeakMap ideal for storing metadata associated with DOM elements or objects where you don't want to control the object's lifetime.

Code Example

This example demonstrates event listener cleanup, a timer leak and its fix, and using WeakMap to store per-element metadata without preventing GC.

// --- Event listener cleanup ---
class Tooltip {
  constructor(trigger, content) {
    this.trigger = trigger;
    this.content = content;
    this.handleMouseEnter = () => this.show(); // arrow function stored for removal
    this.handleMouseLeave = () => this.hide();

    this.trigger.addEventListener("mouseenter", this.handleMouseEnter);
    this.trigger.addEventListener("mouseleave", this.handleMouseLeave);
  }

  show() { console.log("Showing:", this.content); }
  hide() { console.log("Hiding:", this.content); }

  destroy() {
    // Must pass the exact same function reference to removeEventListener
    this.trigger.removeEventListener("mouseenter", this.handleMouseEnter);
    this.trigger.removeEventListener("mouseleave", this.handleMouseLeave);
    console.log("Tooltip destroyed and listeners removed");
  }
}

// --- Timer leak and fix ---
class LiveClock {
  constructor(element) {
    this.element = element;
    this.intervalId = null;
  }

  start() {
    this.intervalId = setInterval(() => {
      this.element.textContent = new Date().toLocaleTimeString();
    }, 1000);
  }

  stop() {
    if (this.intervalId !== null) {
      clearInterval(this.intervalId); // must clear to release the closure
      this.intervalId = null;
      console.log("Clock stopped, interval cleared");
    }
  }
}

// --- WeakMap for per-element metadata ---
const elementMeta = new WeakMap();

function trackElement(element, data) {
  elementMeta.set(element, data); // element is a weak key
}

function getElementMeta(element) {
  return elementMeta.get(element);
}

// Simulate element lifecycle
let btn = document.createElement("button");
trackElement(btn, { clickCount: 0, createdAt: Date.now() });

console.log(getElementMeta(btn)); // { clickCount: 0, createdAt: ... }

// When btn is removed from the DOM and all strong references are gone,
// the WeakMap entry is eligible for garbage collection automatically.
// With a plain Map, you'd have to manually delete the entry.
btn = null; // strong reference gone — GC can collect the element and its WeakMap entry

// WeakSet: track which elements have been initialized
const initialized = new WeakSet();

function initializeWidget(element) {
  if (initialized.has(element)) {
    console.log("Already initialized");
    return;
  }
  initialized.add(element);
  console.log("Widget initialized");
  // when element is GC'd, its WeakSet entry is automatically removed
}

Code Explanation

Tooltip stores event handler functions as instance properties (this.handleMouseEnter). This is critical — removeEventListener requires the exact same function reference that was used in addEventListener. If you pass an anonymous function to addEventListener, you can never remove it because you can't reference it again. By storing the handler, destroy() can cleanly remove both listeners, freeing the closure and allowing the Tooltip instance to be garbage collected.

LiveClock demonstrates timer management. The setInterval callback closes over this.element — as long as the interval runs, element (and the entire LiveClock instance via this) is kept alive. Calling stop() clears the interval and stores null, breaking the closure's hold on these references. Without stop(), the clock and its element would stay in memory forever even after being removed from the page.

The WeakMap example shows elementMeta storing per-element data. With a regular Map, setting btn = null would not allow GC to collect the button — the Map still holds a strong key reference. With WeakMap, the key is held weakly, so when all other references to btn are gone (after btn = null), the button and its metadata entry become eligible for GC automatically. No manual cleanup is needed.

WeakSet provides the same GC-friendly tracking for membership — useful for tracking which DOM elements have been initialized, which events have been handled, or which objects have been processed, without preventing those objects from being collected.

Pattern Highlights

Positive Pattern: Always store event listener function references as instance or closure variables so they can be passed to removeEventListener — anonymous inline functions cannot be removed and create permanent leaks.

⚠️ Neutral Note: WeakMap and WeakSet are not iterable — you cannot loop over their entries or get their size. They are designed purely for weak association, not for storing collections you need to enumerate.

Negative Pattern: Do not add event listeners to window, document, or long-lived global DOM elements from within component logic without storing a cleanup reference — these listeners will outlive the component and hold its entire closure in memory.

Common Mistakes

Mistake 1: Adding a listener with an anonymous function

An anonymous inline function passed to addEventListener can never be removed because there is no reference to it.

element.addEventListener("click", function () {
  doSomething(); // this listener can never be removed
});

element.removeEventListener("click", function () {
  doSomething(); // different function object — removeEventListener does nothing
});

Store the function reference: const handler = () => doSomething(); element.addEventListener("click", handler); and then element.removeEventListener("click", handler) when cleanup is needed.

Mistake 2: Using Map to store element data (preventing GC)

A regular Map holds a strong reference to its keys, preventing GC even when the key object is no longer used anywhere else.

const cache = new Map();

function processElement(el) {
  cache.set(el, { processed: true });
}

// Even after el is removed from the DOM and all other references are gone,
// the Map holds a strong reference — el cannot be garbage collected

Use WeakMap when the key is a DOM element or object whose lifetime you don't control, so the cache entry is automatically freed when the element is collected.

Mistake 3: Not clearing intervals when a component is removed

Setting up an interval without a corresponding cleanup creates a leak that gets worse over time.

function startPolling(userId) {
  setInterval(async () => {
    const data = await fetchUserData(userId);
    updateUI(data); // updateUI may reference a DOM element that no longer exists
  }, 5000);
  // interval never cleared — runs forever even after component is gone
}

Always pair setInterval with clearInterval. In React effects, return a cleanup function; in class components, clear in componentWillUnmount; in vanilla JS, call clearInterval when the component or feature is destroyed.

Quick Recap

  • Memory leaks in JavaScript occur when objects are kept reachable through references that are never cleaned up — common sources are event listeners, setInterval callbacks, and closures capturing large objects.
  • Event listeners must be removed with removeEventListener using the exact same function reference; store handler functions as variables or instance properties, never as anonymous inline functions.
  • WeakMap holds weak references to its keys — when all strong references to a key object are gone, the GC can collect it and the WeakMap entry is automatically removed; use it for per-element or per-object metadata.
  • Always clear setInterval and setTimeout timers when the feature using them is destroyed; an uncleaned interval holds its callback closure alive indefinitely.

Ready to test your understanding? Take the quiz to reinforce what you learned.

© 2026 Ant Skillsv.26.07.07-23:01