Gestión de Memoria
Recolección de basura, fugas de memoria, WeakMap, WeakSet y optimización de rendimiento
Entendiendo la Memoria en JavaScript
JavaScript gestiona automáticamente la memoria a través de la recolección de basura, pero entender cómo funciona te ayuda a escribir código más eficiente y evitar fugas de memoria. Una mala gestión de memoria puede llevar a aplicaciones lentas y crashes.
Ciclo de Vida de la Memoria
- Asignación — Se asigna memoria cuando creas valores
- Uso — Se usa la memoria cuando lees/escribes valores
- Liberación — La memoria se libera cuando ya no es necesaria (GC)
Fundamentos de Recolección de Basura
// Se asigna memoria
let usuario = { nombre: "Alicia" };
let referencia = usuario;
// El objeto aún es alcanzable vía 'referencia'
usuario = null;
// Ahora no existen referencias - GC puede recolectar
referencia = null;
// El objeto { nombre: "Alicia" } será recolectado por el GC
// Algoritmo mark-and-sweep:
// 1. Iniciar desde "raíces" (objeto global, call stack actual)
// 2. Marcar todos los objetos alcanzables
// 3. Barrer (eliminar) objetos no marcados
Fugas de Memoria Comunes
1. Variables Globales Accidentales
// ❌ MAL: Global accidental (falta 'let' o 'const')
function crearUsuario() {
usuario = { nombre: "Bob" }; // ¡Se adjunta a window!
}
// ✅ BIEN: Usa modo estricto y declaraciones apropiadas
"use strict";
function crearUsuario() {
const usuario = { nombre: "Bob" };
return usuario;
}
2. Event Listeners Olvidados
// ❌ MAL: Listeners no removidos
function configurarHandler() {
const boton = document.getElementById("btn");
boton.addEventListener("click", manejarClick);
}
// Después, si el botón se elimina del DOM pero el listener permanece...
// ¡Fuga de memoria!
// ✅ BIEN: Limpiar listeners
class Componente {
constructor() {
this.manejarClick = this.manejarClick.bind(this);
}
montar() {
document.getElementById("btn").addEventListener("click", this.manejarClick);
}
desmontar() {
document.getElementById("btn").removeEventListener("click", this.manejarClick);
}
}
// ✅ MEJOR: Usar AbortController
const controlador = new AbortController();
boton.addEventListener("click", manejarClick, { signal: controlador.signal });
// Limpiar todos los listeners de una vez
controlador.abort();
3. Closures Reteniendo Referencias
// ❌ MAL: Closure retiene datos grandes
function procesarDatos() {
const datosEnormes = new Array(1000000).fill("x");
return function() {
// El closure mantiene datosEnormes en memoria para siempre
return datosEnormes.length;
};
}
const obtenerLongitud = procesarDatos();
// ¡datosEnormes permanece en memoria!
// ✅ BIEN: Solo capturar lo que necesitas
function procesarDatos() {
const datosEnormes = new Array(1000000).fill("x");
const longitud = datosEnormes.length; // Extraer valor necesario
return function() {
return longitud; // Solo captura el número
};
}
4. Timers Olvidados
// ❌ MAL: Timer sigue corriendo después de que el componente desaparece
function iniciarPolling() {
setInterval(() => {
fetch("/api/datos").then(actualizarUI);
}, 1000);
}
// ✅ BIEN: Guardar y limpiar timers
let idPolling;
function iniciarPolling() {
idPolling = setInterval(() => {
fetch("/api/datos").then(actualizarUI);
}, 1000);
}
function detenerPolling() {
clearInterval(idPolling);
}
WeakMap y WeakSet
Las referencias débiles no previenen la recolección de basura:
// Map regular - previene GC
const cache = new Map();
let usuario = { id: 1, nombre: "Alicia" };
cache.set(usuario, "datos cacheados");
usuario = null;
// ¡El objeto aún existe en cache - fuga de memoria!
console.log(cache.size); // 1
// WeakMap - permite GC
const cacheDebil = new WeakMap();
let usuario2 = { id: 2, nombre: "Bob" };
cacheDebil.set(usuario2, "datos cacheados");
usuario2 = null;
// ¡El objeto puede ser recolectado!
// La entrada en cacheDebil se elimina automáticamente
// Casos de uso de WeakMap:
// 1. Datos privados para objetos
const datosPrivados = new WeakMap();
class Usuario {
constructor(nombre, contrasena) {
datosPrivados.set(this, { contrasena });
this.nombre = nombre;
}
verificarContrasena(pwd) {
return datosPrivados.get(this).contrasena === pwd;
}
}
// 2. Cachear valores computados
const computado = new WeakMap();
function obtenerValorCostoso(obj) {
if (!computado.has(obj)) {
computado.set(obj, computacionCostosa(obj));
}
return computado.get(obj);
}
// WeakSet - rastrear objetos sin prevenir GC
const visitados = new WeakSet();
function procesarUnaVez(obj) {
if (visitados.has(obj)) {
return; // Ya procesado
}
visitados.add(obj);
// Procesar objeto...
}
// Cuando obj ya no está referenciado en otro lugar,
// se elimina de visitados automáticamente
WeakRef y FinalizationRegistry
// WeakRef - mantener referencia débil a objeto
let obj = { datos: "importante" };
const refDebil = new WeakRef(obj);
// Después, verificar si aún está disponible
const quizasObj = refDebil.deref();
if (quizasObj) {
console.log(quizasObj.datos);
} else {
console.log("El objeto fue recolectado");
}
// FinalizationRegistry - callback cuando objeto es recolectado
const registro = new FinalizationRegistry((valorRetenido) => {
console.log(`Objeto con ID ${valorRetenido} fue recolectado`);
// Limpiar recursos externos
});
let recurso = { id: 123 };
registro.register(recurso, recurso.id);
recurso = null;
// Eventualmente registra: "Objeto con ID 123 fue recolectado"
Perfilado de Memoria
// Pestaña Memory de Chrome DevTools:
// 1. Tomar heap snapshot
// 2. Grabar asignaciones
// 3. Comparar snapshots para encontrar fugas
// API Performance
console.log(performance.memory); // Solo Chrome
// {
// usedJSHeapSize: 10000000,
// totalJSHeapSize: 35000000,
// jsHeapSizeLimit: 2197815296
// }
// Seguimiento manual de memoria
function obtenerUsoMemoria() {
if (performance.memory) {
return {
usado: Math.round(performance.memory.usedJSHeapSize / 1048576) + " MB",
total: Math.round(performance.memory.totalJSHeapSize / 1048576) + " MB"
};
}
return "No disponible";
}
Mejores Prácticas
// 1. Anular referencias cuando termines
let datosGrandes = cargarDatos();
procesarDatos(datosGrandes);
datosGrandes = null; // Permitir GC
// 2. Usar pools de objetos para asignaciones frecuentes
class ObjectPool {
constructor(crearFn) {
this.pool = [];
this.crear = crearFn;
}
adquirir() {
return this.pool.pop() || this.crear();
}
liberar(obj) {
this.pool.push(obj);
}
}
// 3. Evitar crear objetos en loops
// ❌ MAL
for (let i = 0; i < 1000; i++) {
procesarPunto({ x: i, y: i * 2 });
}
// ✅ BIEN - reusar objeto
const punto = { x: 0, y: 0 };
for (let i = 0; i < 1000; i++) {
punto.x = i;
punto.y = i * 2;
procesarPunto(punto);
}
// 4. Usar typed arrays para datos numéricos grandes
const flotantes = new Float32Array(1000000); // Más eficiente
💡 Puntos Clave
- • JavaScript usa recolección de basura mark-and-sweep
- • Fugas comunes: globales, event listeners, closures, timers
- • Usa WeakMap/WeakSet para caches y metadata
- • Limpia listeners y timers cuando termines
- • Usa la pestaña Memory de Chrome DevTools para encontrar fugas
- • Considera pools de objetos para código crítico de rendimiento
Caza de fugas práctica
- • Simula una fuga: adjunta un event listener a
windowrepetidamente y omite la limpieza; compara heap snapshots. - • Intercambia un cache basado en
MapporWeakMapy confirma que las entradas desaparecen cuando se descartan objetos. - • En DevTools, graba asignaciones mientras navegas entre vistas; busca nodos DOM separados en snapshots.
- • Audita closures de larga vida que capturan objetos grandes o timers y explícitamente anúlalos o límpialos cuando termines.