Entendiendo la Recolección de Basura en JavaScript
La recolección de basura es una función de gestión automática de memoria que garantiza un uso eficiente al liberar la memoria ocupada por objetos que ya no son necesarios.
Introducción a la Recolección de Basura
La recolección de basura gestiona automáticamente la memoria en JavaScript. Libera la memoria utilizada por datos que ya no se necesitan, por lo que casi nunca tienes que asignar o liberar memoria manualmente. Esta página explica la única idea sobre la que se sustenta todo el sistema — la alcanzabilidad — y luego cubre el algoritmo de marcado y barrido, las fugas que aún pueden ocurrir, y cómo WeakMap/WeakSet ayudan a evitarlas.
Entender esto importa porque "automático" no significa "a prueba de fugas". El recolector solo elimina lo que puede demostrar que es inalcanzable; si tu código mantiene viva una referencia oculta, la memoria permanece asignada durante toda la vida del programa.
Alcanzabilidad: el concepto central
El motor no rastrea si un object está "en uso" en un sentido lógico. Rastrea si el object es alcanzable — si existe alguna cadena de referencias que conduzca a él desde una raíz.
Las raíces son valores que el motor siempre conserva:
- Las variables locales y parámetros de la función que se está ejecutando actualmente.
- Variables y funciones en la cadena actual de llamadas anidadas.
- Variables globales (propiedades de
globalThis/window).
Cualquier object alcanzable siguiendo referencias desde una raíz — directamente o a través de otros objetos alcanzables — se conserva. Todo lo demás es basura.
Ejemplo: una referencia mantiene un object vivo
Explicación: El object { name: "John" } era alcanzable a través de user. Copiar la referencia en admin crea un segundo camino hacia él. Establecer user = null elimina un camino, pero admin sigue apuntando al object, por lo que permanece alcanzable y no se recolecta.
Los objetos interconectados también se recolectan
Un mito común es que los objetos que se referencian entre sí sobreviven. No es así — lo que importa es la alcanzabilidad desde una raíz, no si los objetos se apuntan entre sí.
Explicación: obj1 y obj2 forman un ciclo, pero una vez que ambas variables raíz se establecen a null no existe ningún camino desde ninguna raíz hacia el ciclo. Toda la isla se vuelve inalcanzable y es elegible para su recolección. Por eso los motores de JavaScript usan la alcanzabilidad en lugar del conteo de referencias ingenuo, que provocaría fugas en ciclos.
Cómo funciona el recolector: marcado y barrido
Los motores de JavaScript recuperan la memoria con el algoritmo de marcado y barrido. Reduce "este object ya no se necesita" a la pregunta precisa "este object ya no es alcanzable".
- Marcado. A partir de las raíces, el recolector visita cada object alcanzable y lo marca. Luego sigue sus referencias, marca esos objetos, y así sucesivamente hasta que todos los objetos alcanzables están marcados.
- Barrido. Todo object que no fue marcado es inalcanzable. La memoria que ocupa se libera.
No puedes activar esto manualmente y no deberías intentarlo — no existe un gc() estándar en el lenguaje. Los motores reales (como V8) refinan el algoritmo básico con optimizaciones como la recolección generacional (los objetos nuevos mueren jóvenes, por lo que se verifican con más frecuencia) y la recolección incremental (dividir el trabajo en fragmentos para evitar pausas). El modelo de alcanzabilidad sobre el que razonaste anteriormente sigue siendo el mismo.
Fuentes comunes de fugas de memoria
Una fuga en JavaScript es simplemente un object que permanece alcanzable aunque tu programa ya haya terminado con él. El recolector funciona correctamente — simplemente no puede saber que la referencia está obsoleta. Presta atención a estos patrones.
Temporizadores e intervalos olvidados
Un setInterval pendiente (o setTimeout) mantiene vivo su callback, y el callback mantiene vivo todo lo que captura en su cierre. Si nunca llamas a clearInterval, esa memoria se retiene durante toda la vida de la página.
Nodos DOM desconectados
Si eliminas un elemento de la página pero mantienes una referencia a él en una variable, el nodo — y todo su subárbol — no puede ser recolectado.
let detached = document.getElementById('list');
document.body.removeChild(detached);
// The node is gone from the page, but 'detached' still references it,
// so it stays in memory. Release it when done:
detached = null;Escuchadores de eventos persistentes
Un escuchador enlazado a un elemento DOM mantiene alcanzable tanto el elemento como el manejador (con todo lo que captura en su cierre). Elimina los escuchadores con removeEventListener una vez que ya no se necesiten:
<button id="myButton">Click me</button>
<script>
const button = document.getElementById('myButton');
function alertClick() {
alert("Button clicked!");
button.removeEventListener('click', alertClick); // free the listener after first use
}
button.addEventListener('click', alertClick);
</script>El botón solo responde al primer clic: el manejador se elimina a sí mismo con removeEventListener, liberando la referencia para que pueda ser recolectada como basura.
Cachés globales que solo crecen
Un caché almacenado en un object a nivel de módulo o global mantiene cada entrada alcanzable indefinidamente a menos que elimines explícitamente las antiguas. Un Map sin límite utilizado como caché es una fuga lenta clásica.
Cierres que capturan más de lo que crees
Un cierre mantiene viva cada variable en los ámbitos que referencia — incluso aquellas que nunca usa realmente. Devolver una pequeña función interna desde una función con variables locales grandes puede fijar esas variables en memoria. Mantén el ámbito capturado al mínimo, y consulta el ámbito de variables para saber cómo los cierres retienen su entorno.
Cómo ayudan WeakMap y WeakSet
Map y Set mantienen referencias fuertes a sus claves/valores, por lo que cualquier cosa almacenada en ellos permanece alcanzable. WeakMap y WeakSet mantienen sus claves de forma débil: si la única referencia restante a un object es la que hay dentro de un WeakMap, el object puede seguir siendo recolectado, y la entrada desaparece con él.
Esto hace que WeakMap sea ideal para asociar datos adicionales con objetos (cachés, metadatos, gestión de nodos DOM) sin obligar a esos objetos a vivir para siempre. Dado que las entradas pueden desaparecer en cualquier momento, WeakMap/WeakSet deliberadamente no son iterables y no tienen size.
Buenas prácticas
- Prefiere variables locales; salen del ámbito automáticamente y se vuelven recolectables cuando la función retorna.
- Limita las variables globales — viven tanto como la aplicación. Usa módulos y ámbito de bloque (
let/const). - Limpia los temporizadores (
clearInterval/clearTimeout) y elimina los escuchadores de eventos conremoveEventListeneruna vez que ya no se necesiten. - Establece a null las referencias a nodos DOM desconectados y otros objetos grandes con los que hayas terminado.
- Usa
WeakMap/WeakSetpara cachés y metadatos con clave de object para que las entradas no sobrevivan a sus claves.
Conclusión
La recolección de basura en JavaScript se basa completamente en la alcanzabilidad: el motor conserva cualquier object que pueda alcanzar desde una raíz y libera el resto usando marcado y barrido, que maneja incluso los ciclos de referencias. "Automático" aún te deja responsable de no mantener referencias obsoletas — los temporizadores olvidados, los nodos DOM desconectados, los cachés en constante crecimiento y los cierres que capturan demasiado son los culpables habituales. Recurre a WeakMap y WeakSet cuando quieras asociar datos con objetos sin mantenerlos vivos.