Deja de perder memoria con WeakMap
Corrigiendo código débil con referencias débiles
¿Conoces esa sensación cuando cambias una línea de código y ves cómo el uso de memoria cae un 50%? Tuve ese momento mirando el Performance Monitor de Chrome DevTools mientras una aplicación de dashboard pasaba de sangrar 100MB por hora a funcionar limpia toda una tarde.
El cambio de una línea: new Map() se convirtió en new WeakMap().
Eso es todo. Misma superficie de API, mismo patrón de uso, comportamiento completamente distinto bajo el capó. Pero entender por qué funciona significa entender algo en lo que la mayoría de desarrolladores JavaScript nunca piensan: qué pasa cuando nada está mirando tus datos.
Cuando las referencias se convierten en anclas
Un Map normal en JavaScript trata sus claves como carga preciada. Una vez que pones algo ahí dentro, el Map se aferra a ello con puño de hierro. El Garbage Collector ve esta relación y piensa: “Claramente todavía necesitan este objeto, mejor no tocarlo”.
Este instinto protector se convierte en un problema cuando estás almacenando metadatos sobre cosas temporales. Nodos DOM que se eliminan. Sesiones de usuario que expiran. Instancias de componentes que se desmontan. El Map no sabe que estos objetos ya no son útiles. Solo sabe que tiene una referencia, así que los mantiene vivos.
const cache = new Map();
function trackClick(element) { cache.set(element, { clicks: 0 });}
document.body.removeChild(element);// El elemento desapareció del DOM, pero cache lo mantiene en memoriaEl Garbage Collector no puede limpiar element porque cache sigue apuntando a él. Esto se llama una “referencia fuerte,” y en aplicaciones SPA de larga duración, se convierte en una fuga que eventualmente crashea el navegador.
WeakMap cambia las reglas
Un WeakMap funciona de manera diferente. Trata sus claves como ciudadanos temporales en vez de residentes permanentes. Cuando almacenas algo en un WeakMap, estás diciendo esencialmente: “Quiero asociar estos datos con este objeto, pero no quiero ser la razón por la que siga vivo.”
Si lo único que mantiene un objeto en memoria es un WeakMap, el Garbage Collector tiene permitido llevárselo. Cuando el objeto desaparece, la entrada del WeakMap desaparece con él. No se necesita limpieza manual.
const cache = new WeakMap();
function trackClick(element) { cache.set(element, { clicks: 0 });}
document.body.removeChild(element);// El elemento es recolectado por el Garbage Collector// La entrada del cache desaparece automáticamenteEjecuté un benchmark creando 100,000 nodos DOM, almacenando metadatos sobre cada uno, y luego eliminándolos todos. Con un Map, el navegador retenía entre 150-200MB. Con un WeakMap, bajaba a 70-80MB. Mismo código, misma funcionalidad, la mitad del consumo de memoria.
Lo que renuncias
WeakMap tiene restricciones que parecen limitaciones hasta que te das cuenta de que son lo que hace que la magia funcione.
No puedes iterar sobre un WeakMap. No hay forEach, no hay keys(), no hay values(). Esto tiene sentido cuando lo piensas: el Garbage Collector podría eliminar una entrada en medio de tu bucle. ¿Realmente quieres lidiar con eso?
No puedes verificar el tamaño. No hay propiedad .size, no hay .length. De nuevo, es un blanco en movimiento. El número podría cambiar entre cuando preguntas y cuando obtienes la respuesta.
Las claves deben ser objetos. Ni cadenas, ni números, ni primitivos. Esto es fundamental para cómo funcionan las referencias débiles: los valores primitivos no tienen una identidad que pueda rastrearse separadamente de su valor.
Estos no son bugs. Son el diseño. WeakMap está construido para un trabajo específico: adjuntar metadatos a objetos sin impedir que esos objetos sean limpiados. Si necesitas iteración, claves primitivas o un conteo de entradas, probablemente estés resolviendo un problema diferente y deberías usar un Map normal.
dónde ayuda realmente
El patrón de “datos privados” fue el caso de uso original de WeakMap, antes de que JavaScript tuviera campos #private. Las librerías creaban un WeakMap fuera de la clase y lo usaban para almacenar datos que no deberían ser accesibles en la instancia.
const privateData = new WeakMap();
class User { constructor(name) { privateData.set(this, { name }); }
getName() { return privateData.get(this).name; }}Cuando una instancia de User es recolectada por el Garbage Collector, los datos privados se van con ella. No se necesita código de limpieza.
La memoización es otro caso natural, especialmente cuando estás cacheando resultados basados en entradas de objetos en vez de valores primitivos. Si tu cálculo costoso toma un objeto de configuración como entrada, un WeakMap significa que no tienes que preocuparte de que el cache sobreviva a las configuraciones.
const cache = new WeakMap();
function expensiveCalc(obj) { if (cache.has(obj)) return cache.get(obj);
const result = heavyMath(obj); cache.set(obj, result); return result;}El cache solo vive mientras los objetos cacheados. Una vez que obj no es referenciado en ningún otro lugar, tanto el resultado cacheado como la entrada del cache desaparecen juntos.
Cuándo recurrir a él
Las fugas de memoria en aplicaciones web modernas suelen provenir de referencias obsoletas a cosas que deberían haberse limpiado. Si estás construyendo algo de larga duración — un dashboard que permanece abierto todo el día, una app de chat que corre por horas, un panel de administración que nunca se refresca — necesitas pensar en qué pasa con los datos antiguos.
WeakMap es particularmente útil cuando estás asociando datos con nodos DOM, instancias de componentes, o cualquier objeto cuyo tiempo de vida no controlas. Si estás almacenando algo basado en una referencia y esa referencia podría desaparecer, WeakMap hace la limpieza mucho más simple.
Un Map normal sigue siendo la elección correcta cuando estás construyendo un cache real con políticas de expulsión, cuando necesitas iterar sobre entradas, cuando estás usando claves primitivas, o cuando los datos en sí son lo que importa en vez de su asociación con un objeto.
Lo bueno de WeakMap es que normalmente es obvio cuándo lo necesitas. Si te encuentras escribiendo código de limpieza para eliminar entradas de un map cuando los objetos se destruyen, esa es una señal. Si te preocupa que la memoria crezca sin límite porque no sabes cuándo eliminar cosas, esa es otra señal.
A veces la mejor característica es la que simplemente funciona sin que tengas que pensar en ella.
Recursos
- MDN: WeakMap
- MDN: Gestión de memoria
- V8 Blog: Weak References y Finalizers
- JavaScript.info: WeakMap y WeakSet