Kodokon kodokon.com

Memoria y rendimiento: GC, WeakMap/WeakRef, debounce/throttle

Comprende cómo funciona el recolector de basura, usa referencias débiles para evitar fugas y suaviza el trabajo costoso con debounce y throttle.

11 min · 3 preguntas

Abrir esta lección en Kodokon

El recolector de basura de V8 se basa en la alcanzabilidad: un objeto se vuelve recolectable en cuanto ninguna cadena de referencias lo enlaza con las raíces (la pila, los ámbitos activos, los globales). El GC es generacional: los objetos jóvenes viven en la *nursery*, recolectada a menudo y rápidamente (scavenge); los supervivientes se promueven a la generación antigua, gestionada por un mark-and-sweep incremental. Las fugas clásicas no provienen del GC sino de referencias que olvidaste: cachés basadas en Map, listeners que nunca se desvinculan, closures que capturan objetos grandes.

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
El mismo código, dos destinos de memoria opuestos.

Un WeakMap no retiene sus claves: cuando una clave deja de ser alcanzable desde cualquier otro lugar, la entrada entera desaparece. Consecuencias de diseño: las claves deben ser objetos (o símbolos no registrados), y la estructura no es ni iterable ni medible - exponer su tamaño revelaría el comportamiento del GC, que no es determinista. WeakRef va más allá: te da una referencia débil directa, que se desreferencia con 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() puede devolver el objeto... o undefined.

Del lado del rendimiento percibido, los eventos de alta frecuencia (scroll, resize, input, mousemove) pueden desencadenar cientos de llamadas por segundo. Dos estrategias complementarias: debounce solo ejecuta la función después de un periodo de calma (ideal para la búsqueda mientras se escribe), mientras que throttle garantiza como máximo una ejecución por intervalo (ideal para seguir el desplazamiento).

JAVASCRIPT
function debounce(fn, delay) {
  let timer = null;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(
      () => fn.apply(this, args),
      delay
    );
  };
}
Debounce: solo cuenta la última llamada de una ráfaga.
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: como máximo una ejecución por intervalo.

Prueba de conocimientos

Comprueba que has retenido los puntos clave de esta lección.

  1. ¿Por qué las claves de un WeakMap deben ser objetos y no primitivos como cadenas?
    • Por razones de rendimiento de hashing
    • Porque solo una referencia a un objeto puede volverse inalcanzable y desencadenar la eliminación de la entrada
    • Es una limitación histórica levantada desde ES2021
    • Porque los primitivos no pueden usarse como claves en JavaScript
  2. ¿Cuál es la diferencia fundamental entre debounce y throttle?
    • Debounce se ejecuta tras un periodo de calma, throttle garantiza como máximo una ejecución por intervalo
    • Debounce es asíncrono, throttle es síncrono
    • Throttle cancela las llamadas anteriores, debounce las encola
    • Son dos nombres para la misma técnica
  3. ¿Qué puede devolver ref.deref() sobre un WeakRef?
    • Siempre el objeto original, mientras exista el WeakRef
    • El objeto si sigue vivo, o undefined si ha sido recolectado
    • Una copia profunda del objeto
    • null si el objeto ha sido recolectado