TechLead

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

  1. Asignación — Se asigna memoria cuando creas valores
  2. Uso — Se usa la memoria cuando lees/escribes valores
  3. 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 window repetidamente y omite la limpieza; compara heap snapshots.
  • • Intercambia un cache basado en Map por WeakMap y 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.