Fugas de memoria y limpieza de recursos en JavaScript | Nicolás Garzón
- Memoria crece después de repetir una acción.
- Volver al estado inicial no libera una cantidad razonable.
- La aplicación se vuelve más lenta con el tiempo.
- Cada navegación duplica listeners o timers.
- Permanecen nodos desconectados.
- Una cache nunca reduce tamaño.
JavaScript
function save(value) {
leakedValue = value;
}
Puede crear una propiedad global y conservar datos durante toda la sesión.
Módulos y strict mode convierten esta asignación en error.
JavaScript
const cache = new Map();
function remember(key, value) {
cache.set(key, value);
}
Si las claves nunca se retiran, la cache es almacenamiento permanente.
- Límite.
- TTL.
- Invalidación.
- LRU.
- Cleanup por evento.
JavaScript
function mount() {
window.addEventListener("resize", updateLayout);
}
Llamar mount varias veces registra varias callbacks.
JavaScript
function mount() {
const controller = new AbortController();
window.addEventListener("resize", updateLayout, {
signal: controller.signal,
});
return () => controller.abort();
}
JavaScript
button.addEventListener("click", () => save());
No puedes retirarlo después sin conservar la referencia o utilizar signal.
JavaScript
const intervalId = setInterval(refresh, 1000);
Un interval olvidado puede seguir ejecutando trabajo y reteniendo closures.
JavaScript
clearInterval(intervalId);
JavaScript
const observer = new ResizeObserver(updateLayout);
observer.observe(element);
JavaScript
observer.disconnect();
Aplica también a MutationObserver e IntersectionObserver.
JavaScript
const unsubscribe = store.subscribe(listener);
Quien se suscribe debe conocer cuándo llamar unsubscribe.
Una API que retorna cleanup hace visible el ciclo de vida.
JavaScript
function createHandler() {
const dataset = loadLargeDataset();
return () => {
console.log(dataset.length);
};
}
Si solo necesitas la longitud:
JavaScript
function createHandler() {
const length = loadLargeDataset().length;
return () => console.log(length);
}
No optimices closures por reflejo; identifica qué binding realmente mantiene una estructura grande.
JavaScript
const removedPanels = [];
function removePanel(panel) {
panel.remove();
removedPanels.push(panel);
}
La colección conserva cada nodo retirado.
JavaScript
const url = URL.createObjectURL(file);
image.src = url;
JavaScript
URL.revokeObjectURL(url);
No revoques antes de que el consumidor termine.
JavaScript
const worker = new Worker("worker.js");
Debe terminarse cuando su tiempo de vida no coincide con toda la aplicación.
JavaScript
worker.terminate();
JavaScript
const reader = response.body.getReader();
Si abandonas lectura, libera el lock o cancela cuando corresponda.
JavaScript
await reader.cancel();
reader.releaseLock();
JavaScript
function startFeature() {
const controller = new AbortController();
const { signal } = controller;
window.addEventListener("resize", onResize, {
signal,
});
button.addEventListener("click", onClick, {
signal,
});
void loadData({ signal }).catch(handleAbortOrError);
return () => controller.abort();
}
Una señal coordina listeners y operaciones cancelables.
Para recursos importantes:
JavaScript
const connection = await openConnection();
try {
await useConnection(connection);
} finally {
await connection.close();
}
No dependas del garbage collector para liberar recursos externos.
El lenguaje moderno define símbolos y sintaxis de disposición para recursos que implementan contratos apropiados.
JavaScript
using resource = createDisposable();
JavaScript
await using resource = await createAsyncDisposable();
El soporte depende del runtime y tooling objetivo. El principio es que el scope controla la disposición, no el momento del GC.
Cambiar una Map por WeakMap solo funciona cuando:
- Las claves son objetos adecuados.
- No necesitas enumerar.
- El valor debe vivir mientras viva la clave.
No soluciona timers, listeners o caches por ID string.
- Reproduce una acción repetible.
- Toma un heap snapshot inicial.
- Ejecuta y revierte la acción varias veces.
- Fuerza GC desde DevTools solo para diagnóstico.
- Toma otro snapshot.
- Compara dominators y retaining paths.
La ruta de retención muestra qué referencia mantiene vivo el objeto.
JavaScript
function mountProductPanel(element) {
const controller = new AbortController();
const observer = new ResizeObserver(updateLayout);
const worker = new Worker("search-worker.js");
observer.observe(element);
window.addEventListener("keydown", handleKeydown, {
signal: controller.signal,
});
return () => {
controller.abort();
observer.disconnect();
worker.terminate();
};
}
Cada recurso tiene una ruta de cierre.
- Llamar fuga a cualquier consumo alto.
- Crear caches sin límites.
- Registrar listeners repetidamente.
- Olvidar intervals y observers.
- Conservar nodos retirados.
- No revocar object URLs.
- Depender del GC para cerrar recursos externos.
- Usar WeakMap como parche universal.
- Mirar solo el tamaño y no la retaining path.
- Añadir cleanup que nunca se ejecuta.
- Una fuga es retención innecesaria, no solo memoria alta.
- Los recursos de larga vida necesitan owner y cleanup.
- Caches deben tener política de reducción.
- Listeners requieren identidad, signal o unsubscribe.
- DOM retirado puede seguir retenido.
- Garbage collection no sustituye close, abort o dispose.
- Heap snapshots ayudan a seguir rutas de retención.
- Una API bien diseñada devuelve su cleanup.
¿Por qué WeakMap no soluciona una cache indexada por IDs string?
Respuesta
Porque WeakMap necesita claves débiles como objetos admitidos y no sirve para enumerar o gestionar una cache por claves primitivas.
Rendimiento y capacidad de respuesta diferencia velocidad total, latencia y fluidez de la interfaz.