Kodokon kodokon.com

Memory and performance: GC, WeakMap/WeakRef, debounce/throttle

Understand how the garbage collector works, use weak references to avoid leaks, and smooth out expensive work with debounce and throttle.

11 min · 3 questions

Open this lesson in Kodokon

V8's garbage collector is based on reachability: an object becomes collectable as soon as no chain of references links it to the roots (the stack, active scopes, globals). The GC is generational: young objects live in the *nursery*, collected often and quickly (scavenge); survivors are promoted to the old generation, handled by an incremental mark-and-sweep. Classic leaks do not come from the GC but from references you forgot: Map-based caches, listeners that are never detached, closures capturing large objects.

JAVASCRIPT
const strongCache = new Map();
const weakCache = new WeakMap();

let session = { user: "ada" };
strongCache.set(session, "data");
weakCache.set(session, "data");

session = null;
// Map: the key retains the object -> memory leak
// WeakMap: the entry becomes collectable
Same code, two opposite memory fates.

A WeakMap does not retain its keys: when a key becomes unreachable from anywhere else, the whole entry disappears. Design consequences: keys must be objects (or unregistered symbols), and the structure is neither iterable nor measurable - exposing its size would reveal the behavior of the GC, which is non-deterministic. WeakRef goes further: it gives you a direct weak reference, to be dereferenced with deref().

JAVASCRIPT
let config = { theme: "dark" };
const ref = new WeakRef(config);

function readTheme() {
  const target = ref.deref();
  return target ? target.theme : "default";
}

console.log(readTheme()); // "dark"
config = null;
// After a GC pass, deref() may
// return undefined: plan a fallback.
deref() may return the object... or undefined.

On the perceived-performance side, high-frequency events (scroll, resize, input, mousemove) can trigger hundreds of calls per second. Two complementary strategies: debounce only runs the function after a quiet period (ideal for search-as-you-type), while throttle guarantees at most one execution per interval (ideal for tracking scrolling).

JAVASCRIPT
function debounce(fn, delay) {
  let timer = null;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(
      () => fn.apply(this, args),
      delay
    );
  };
}
Debounce: only the last call of a burst counts.
JAVASCRIPT
function throttle(fn, interval) {
  let last = 0;
  return function (...args) {
    const now = Date.now();
    if (now - last >= interval) {
      last = now;
      fn.apply(this, args);
    }
  };
}
Throttle: at most one execution per interval.

Knowledge check

Make sure you remember the key points of this lesson.

  1. Why must WeakMap keys be objects rather than primitives like strings?
    • For hashing performance reasons
    • Because only a reference to an object can become unreachable and trigger the removal of the entry
    • It is a historical limitation lifted since ES2021
    • Because primitives cannot be used as keys in JavaScript
  2. What is the fundamental difference between debounce and throttle?
    • Debounce runs after a quiet period, throttle guarantees at most one execution per interval
    • Debounce is asynchronous, throttle is synchronous
    • Throttle cancels previous calls, debounce queues them
    • They are two names for the same technique
  3. What can ref.deref() return on a WeakRef?
    • Always the original object, as long as the WeakRef exists
    • The object if it is still alive, or undefined if it has been collected
    • A deep copy of the object
    • null if the object has been collected