Optimización de Memoria en JS: WeakMap
🎵Si no me acuerdo, no pasó 🎵
Una artista por ahí.
Uno de los principios que pasan inadvertidos cuando trabajamos con JavaScript es, y por mucho margen, el manejo de la memoria. Quizás en este momento, despues de leer la línea anterior, comenzaste a dudar y a plantearte, si has sido descuidado con tu código y si hubieras podido hacer mejor las cosas.
Te puedo decir que si nunca habías pensado en esto antes, es probable que no lo necesitaras, bien sea porque no llegaste a notar problemas de rendimiento, o porque simplemente no ha sido tu prioridad.
JavaScript es bastante “eficiente” cuando de recolección de basura se refiere (Garbage Collection), y llevará a cabo dicha tarea sin necesidad que tú se lo digas.
Pero hay escenarios donde es necesario dar ese “paso extra” para evitar tener esa fuga de memoria y que nuestra aplicación no colapse por un descuido.
Que es WeakMap
WeakMap es una colección de tuplas clave-valor, donde las claves deben ser objetos, y los valores pueden ser cualquier valor arbitrario. Hasta aquí son idénticas a Map.
Sin embargo, las referencias clave-valor en WeakMap son “débiles”, y con débiles, significa que cuando la referencia a una clave de un WeakMap desaparece, la recolección de basura hace su trabajo y reclama el espacio de memoria que ocupa el valor en el WeakMap.
Otra característica de WeakMap, es que sus claves no son enumerables y tampoco permite conocer su tamaño o iterar por sus elementos.
Veamos un ejemplo para explicar la teoría, primero con Map.
// Map normal, usamos un poco de TS para embellecer el ejemplo
// Creamos un Map, donde las claves serán strings, y los valores pueden almacenar cualquier tipo
const families = new Map<string, any>();
const smith = 'Smith';
families.set(smith, ['Brad', 'Angelina']);
// Map permite recuperar las claves
console.log('Claves de Familias:', Array.from(familias.keys()).join(', '));
// Claves de Familias: Smith
// Map permite iterar sobre sus elementos
for(let [key, value] of families) {
console.log(`Familia de ${key}: ${value}`);
// Familia de Smith: Brad,Angelina
}
Y ahora con WeakMap
// Definimos nuestras claves
const mamiferos = { especie: 'Mamiferos' };
let reptiles = { especie: 'Reptiles' };
especies.set(mamiferos, ['Humanos', 'Perros', 'Caballos']);
especies.set(reptiles, ['Lagartos', 'Serpientes']);
// Podemos obtener el valor si conservamos la referencia a las claves
console.log(especies.get(mamiferos).join(', ')); // Humanos, Perros, Caballos
console.log(especies.get(reptiles).join(', ')); // Lagartos, Serpientes
// Pero si eliminamos la referencia a la clave
reptiles = null;
// Perdemos el valor, ya que no tenemos acceso a keys(), values() o entries()
console.log(especies.get(reptiles).join(', '));
Como podemos ver con WeakMap, una vez se elimina la referencia a la clave, no es posible recuperar el valor, y esto hace que el recolector de basura haga su trabajo. Debido a que no sabemos en qué momento se recoja la basura, es lógico suponer el porqué métodos como keys, values o entries no estén disponibles en WeakMap.
Casos de uso para WeakMap
Podemos pensar en varios casos de uso para WeakMap, pero de los primeros que se me vienen a la memoria es el uso como caché.
Supongamos que vamos a almacenar el resultado de una operación sobre un objeto en caché, de tal manera que posteriores ejecuciones de la operación acudan al resultado y no ejecuten de nuevo la operación completa.
class MiClaseCache {
static cacheOperacion = new Map<object, any>();
public operacionCacheable() {
if (!MiClaseCache.cacheOperacion.has(this)) {
const resultado = /* ... operación ... */;
MiClaseCache.cacheOperacion.set(this, resultado);
return resultado;
}
return MiClaseCache.cacheOperacion.get(this);
}
}
let miObjeto = new MiClaseCache();
const resultado1 = miObjeto.operacionCacheable(); // Cálculo y almacena
const resultado2 = miObjeto.operacionCacheable(); // Obtiene valor de cacheOperacion
miObjeto = null;
console.log('Tamano de Cache: ', MiClaseCache.cacheOperacion.size); // Tamaño de Caché: 1
Hasta aquí funciona perfecto, pero si el objeto se elimina igual se conserva la información en Map, por lo cual habría que manualmente limpiar la caché en todas las partes donde se elimine el objeto, algo engorroso.
Ahora veamos como sería con WeakMap
class MiClaseWeak {
static cacheOperacion = new WeakMap<object, any>();
public operacionCacheable() {
if (!MiClaseWeak.cacheOperacion.has(this)) {
const resultado = /* ... operacion ... */;
MiClaseWeak.cacheOperacion.set(this, resultado);
return resultado;
}
return MiClaseWeak.cacheOperacion.get(this);
}
}
let miObjetoWeak = new MiClaseWeak();
const resultadoWeak1 = miObjetoWeak.operacionCacheable(); // Calculo y almacena
const resultadoWeak2 = miObjetoWeak.operacionCacheable(); // Obtiene valor de cacheOperacion
miObjetoWeak = null;
console.log('Tamano de Cache Weak: ', MiClaseWeak.cacheOperacion.size); // Tamano de Cache: undefined
En este último ejemplo, ya no necesitamos manualmente eliminar la caché del objeto, ya que al eliminar la referencia JavaScript se encargara de procesar el WeakMap. Veamos que no podemos tampoco contar el tamaño de nuestro caché.
Basándonos en el ejemplo anterior, podríamos inferir incluso que se puede usar como medida de “privacidad”.
let usuario = {id: 'pedro01'};
const infoProvisionalUsuario = new WeakMap<object, any>();
infoProvisionalUsuario.set(usuario, {tarjetaCredito: '0000000000000000'});
// ... ya hicimos la operacion de compra, no necesitamos al usuario de nuevo
usuario = null;
// la informacion que teniamos ya no es accesible porque no tenemos la referencia al usuario
Y como podemos apreciar, hemos añadido algo de privacidad a nuestro código, y escondimos de manera elegante información sensible que no es necesaria ya para nuestra ejecución.
Creo que esta es una de esas funcionalidades o técnicas escondidas en JavaScript y de las que no muchos hablamos, pero que si la empleamos bien podemos escribir código con mejores resultados en rendimiento y tal vez seguridad.