Implementando un cache en JavaScript

Un caché es básicamente una entidad que almacena información de manera transparente con el objetivo de reducir los costos de cómputos de operaciones futuras.

En el ámbito web el uso de caché es conocido en cuanto al servicio de contenido, que suele ser cacheado a fines de agilizar futuros requerimientos del mismo.

En general el uso de caché permite agilizar los procesos así como también reducir los costos operativos, lo cual decanta en una mejor experiencia de usuario.

Utilizando un cache básico

Suponiendo que tenemos una implementación bastante básica de caché con la siguiente interfaz:

cache.set(key, value)
cache.get(key)

Podemos modificar nuestro código para reducir las llamadas a operaciones costosas, reduciendo virtualmente en un 100% el trabajo requerido para las solicitudes adicionales. Caso de uso típico:

// Sin cache, se realiza siempre la
// operacion costosa
data = expensiveOperation();

// Con cache
data = cache.get('data');

// La operacion costosa solo se realiza en
// caso de que no exista la informacion en
// cache
if(!data) {
	data = expensiveOperation();
	cache.set('data', data);
}

Un ejemplo más concreto se puede dar con las consultas ajax. Suponiendo que nuestra aplicación realiza numerosos requests, podríamos generar un proxy para nuestros métodos ajax a fines de no realizar requests repetidos (algo bastante común en cualquier aplicación web).

Suponiendo que tenemos un método encargado de realizar las consultas:

// Fachada original para las consultas de la aplicacion
myApp.request = function(url, opts) {
    $.post(url, opts || {});
};

// Caso de uso
myApp.request('user.model', {
    success: function(data) {
        myApp.render(data);
    }
});

Siendo que alguna de las consultas no es necesario repetirlas, podemos implementar la funcionalidad que gestione el cache de manera transparente para el resto de la aplicación:

// Fachada para las consultas de la aplicacion
myApp.request = function(url, opts) {
    opts = opts || {};

    // Nos aseguramos de que exista un callback
    var callback = opts.success || function(data) {};        

    // Si existe en cache, nos evitamos la consulta
    if(opts.cache !== false && myApp.cache.get(service)) {
        opts.success(myApp.cache.get(service));
        return;
    }

    // Envolvemos el callback para cachear el resultado
    opts.success = function(data) {
        cache.set(service, data);
        callback(data);
    };

    // Finalmente, realizamos la consulta
    $.post(url, opts);
};

De esta manera, tenemos una interfaz idéntica con la funcionalidad de caché con lo cual el impacto en el resto de la aplicación es nulo, permitiendo además alivianar la carga sobre el servidor, así como también reducir los tiempos de espera para el usuario.
Obviamente nada es gratis, si la información esta cacheada, está ocupando memoria, por lo cual hay que considerar el balance entre costo de cómputo y costo de memoria a la hora de implementar un caché.

Impacto de cache contra requests

Implementando un cache en memoria

Una implementación básica de caché puede utilizar un objeto en memoria para almacenar los datos. La desventaja de esto es que expirará toda la información al refrescar el sitio, pero aún así es muy útil en aplicaciones web de un solo documento.

Este tipo de caché es muy utilizado en las librerías DOM, ya que almacenan referencia a distintos objetos con el fin de agilizar futuros accesos.

Utilizando la forma clásica de constructores, creamos el constructor y luego agregamos sus métodos al prototipo:

/**
 * @constructor
 */
function Cache() {
    this._data = {};
}

Y luego le agregamos los métodos:

/**
 * Agrega o reemplaza un elemento del
 * cache
 *
 * @param {String} key
 * @param {Object} value
 */
Cache.prototype.set = function(key, value) {
    this._data[key] = value;
};

/**
 * Agrega o reemplaza un elemento del
 * cache
 *
 * @param {String} key
 * @return {Object} value
 */
Cache.prototype.get = function(key) {
    return this._data[key];
};

Con esta implementación, ya podemos realizar las operaciones básicas:

// Instanciamos nuestro cache
var cache = new Cache();

// Almacenamos informacion
cache.set('person', 'John Doe');

// Y la obtenemos
console.log(cache.get('person')); // "John Doe"

Implementando un cache en cookies

A diferencia de la implementación en memoria, usar cookies tiene la ventaja de que los datos pueden persistir en el cliente. Obviamente hay que tomar en cuenta la limitación de tamaño y cantidad *de las cookies así como también el hecho de que las mismas son envíadas al servidor en cada request con su consecuente impacto en la performance.

*Lectura interesante: Browser cookie restrictions (Nicholas C. Zakas)

Otro aspecto a tener en cuenta es el formato en el cual se almacenan las cookies, que implica que todo lo que se guarde lo hara como string, por lo cual previamente se deberá serializar la información (momento en el cual entra en acción JSON). En este ejemplo, el manejo de cookies es el utilizado en quirksmode en el artículo Javascript – Cookies.

Código, sin comentarios esta vez:

function Cache() {
}

Cache.prototype.set = function(key, value) {
	// Requerimos JSON
	if(typeof JSON == 'undefined' || !JSON.stringify) {
		throw 'JSON api is required!';
	}

	value = JSON.stringify(value);

	document.cookie = key+"="+value+"; path=/";
};

Cache.prototype.get = function(key) {
	// Requerimos JSON
	if(typeof JSON == 'undefined' || !JSON.parse) {
		throw 'JSON api is required!';
	}	

	key += "=";

	var
			ca = document.cookie.split(';'),
			c;

	for(var i = 0, l = ca.length; i < l; i++) {
		c = ca[i];

		while (c.charAt(0) == ' ') c = c.substring(1, c.length);

		if (c.indexOf(key) == 0)
			return JSON.parse(c.substring(key.length, c.length));
	}

	return null;
};

Luego, el uso es igual al del ejemplo anterior:

var c = new Cache;

c.set('myArray', [1, 2, 3]);

c.get('myArray').length; // 3

Implementando un cache en local storage

Dentro de las novedades que trae la especificación de HTML5 es la de la definición del ‘Web Storage‘, que básicamente provee un API nativa para almacenar datos en cliente sin las desventajas que presentan las cookies (limite de tamaño, overhead en los requests, etc).

De esta manera, el uso de esta funcionalidad es ideal para implementar el caché de una aplicación, ya que es persistente y el limite de tamaño no es un problema (si bien la especificación no dicta nada actualmente, es bastante consistente entre los browsers: 5mb).

Una interesante implementación de esta API es Kizzy, una librería de Dustin Diaz:

Kizzy is a light-weight, cross-browser, JavaScript local storage utility. It leverages the HTML5 localStorage API when available, as well as Internet Explorer’s persistent XML store — wrapped up in a easy to use, memcached-like interface. When neither of these features are available (unlikely), it falls back to an in-browser object store.

Kizzy presenta un api similar a las implementaciones de caché anteriores, presentando un par de sutiles diferencias. En primer lugar la instanciación de un cache se da bajo un namespace, lo cual permite obtener referencias a un cache en particular mediante su nombre:

// 'users' representa un identificador unico
// para el cache
var cache = kizzy('users');

Y por otro lado, en la declaración de valores, es posible definir el tiempo de expiración del mismo:

// Agregamos un valor al cache, cuya validez
// es de 60 segundos (60 * 1000 ms)
cache.set('model.list', data.list, 60 * 1000);

El código fuente de Kizzy puede encontrarse en su repositorio en GitHub: Kizzy.

Mas funcionalidades

Las implementaciones de caché en memoria y cookies son lo suficientemente funcionales como para ser utilizadas seriamente, pero de igual manera se puede considerar sumarle funcionalidades, como ser:

  • Expiración por tiempo (digamos, que el dato x es válido para solo 10 minutos)
  • Expiración por requests (digamos, que el dato x es válido para solo 3 pedidos)
  • Invalidación total
  • Soporte para los distintos tipos de caché en una misma API
  • etc..

Leer más:

 


Shortlink: http://goo.gl/FP1NL




3 views shared on this article. Join in...

  1. José dice:

    ¡Bravo! Una genial lectura. Detallada y muy didáctica. Me has enseñado a cómo se debe cachear correctamente

    Muchísimas gracias!

  2. Esto es hermoso! :)
    Ahuevo… Muchas Gracias :)

  3. Jaime Cruz dice:

    Buen contenido claro y preciso gracias me va a servir!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos necesarios están marcados *

Comment

You may use these tags : <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>