Mejorando la carga de recursos en la web con localStorage

El mes pasado hablamos de navegadores y sus características avanzadas para utilizar las mejoras HTML5 y CSS3, entre otras. Esta vez vamos a hablar de localStorage, una mejora que utiliza una de las características que está implementada en HTML5 (o mejor dicho que está definido en HTML5 e implementado por cada navegador que quiera cumplir con el estándar).

¿Qué es localStorage?

localStorage es una característica de HTML5, aunque es tan común que podemos encontrarla incluso en mi denostrado Internet Explorer 8 (si recordáis del otro artículo es la máxima versión de Internet Explorer que se puede tener con Windows XP / Vista), así que no tendremos problema en encontrarlo en nuestro navegador. Obviamente en Chrome, Firefox, Opera, Safari lo tenemos.

Esta característica permite almacenar datos en el navegador, por dominio (por dominio significa que los datos que almacenas son propios de cada dominio. Es como si tuvieras un localStorage diferente por dominio). Cada navegador implementa una cantidad máxima de espacio en el localStorage, y varía bastante:

  • Google Chrome 2.5 MB
  • Mozilla Firefox 5 MB
  • Opera 5 MB
  • Internet Explorer 10 MB

Se trata de una base de datos clave/valor (salvando las distancias, similar a Redis). Es decir, asociado a una clave (una cadena) podemos guardar un valor (otra cadena).

Para guardar:

localStorage.setItem(‘foo’, ‘bar’)

Para recuperar el valor:

var valor = localStorage.getItem(‘foo’)
/* valor = ‘bar’ */

Sencillo pero efectivo. Si queréis más información podéis consultar este otro artículo. En resumen os diré que es algo que se utiliza, por ejemplo, para dar soporte a aplicaciones web que pueden funcionar off-line, o para sustituir el uso de cookies, que es una información que viene y va entre el cliente y servidor, y ocupa espacio en cada petición, además de que las cookies están muy limitadas en la cantidad de información que pueden almacenar, además de que hay que cifrarlas si la información contenida es “sensible”.

El origen del problema

Bien, en Sapns se ha incluido algo para mejorar la carga de recursos en un página web, básicamente JavaScript, aunque también se puede utilizar para cargar archivos CSS.

Basado en (copiado de, realmente) una librería llamada basket.js. El funcionamiento de aquella es igual a esta que he creado para Sapns. La estaría utilizando si la hubiera hecho funcionar pero tenía problemas con uno de los componentes y no lo consegu. La idea es suya. Nunca diré que me inventé esto.

La idea de basket.js es la siguiente. En lugar de utilizar el típico tag script

script src="/foo/bar"

utilizamos la librería, que hace las veces de “cargador”:

   _loader = new sapnsLoader();
   _loader.load(
       { url: “/foo/bar/js/mylibrary.js” }
  );

Lo que ocurre es lo siguiente:

  1. Se busca en localStorage si ya hemos cargado esa librería antes
  2. Si no la hemos cargado, la cargamos desde su URL y la guardamos en localStorage. Por contra, si ya existe la recuperamos.
  3. ¿Existe esa librería en el head de la página?
  4. Si no existe (*) injectamos dinámicamente un script en la cabecera (head) de la página con el contenido de la librería. En otro caso, no hacemos nada.

(*) Se sabe que existe porque se le pone un id al script que es igual a la URL del recurso. Por ejemplo:


  ...

Después podemos hacer:

var script = document.getElementyById(url)

o al modo jQuery

var script = $(‘#’+url);
if (!script) {
/* Inyectar script */
}

Muchos dirán “pero si eso ya lo hace el navegador…”. Cierto. Los navegadores cachean los ficheros CSS, Imágenes (PNG, JPG, GIF, etc) y JavaScript que se cargan “externamente”.

link rel=”stylesheet” href=”/foo/bar/css/styles.css”
script src=”/foo/bar/js/mylibrary.js”/script

Cuando, el navegador detecta que en el tiempo determinado por la validez de la cache, la URL no ha cambiado, no se carga el recurso sino que se “recupera”. Como basket.js (o sapnsLoader).

En la imagen son los recursos que están marcados con color anaranjado. Fijaos que el status de la petición es 304 Not Modified. Habla por si solo.

localstorage

El problema surge al utilizar otra cosa…

En Atenea (uno de los sistemas basados en Sapns) se utiliza mucho algo que nos ha hecho llegar hasta aquí: injectar HTML, CSS y JavaScript en la página actual utilizando jQuery.

$.ajax({
    url: “/foo/bar/algun-dialogo/”,
    success: function(content) {
        $(‘#un-dialogo-cualquiera’).html(content).dialog({
            title: “Soy un diálogo”,
            modal: true,
            resizable: false,
            buttons: {
                “Cerrar”: function() {
                    $(‘#un-dialogo-cualquiera’).dialog(‘close’);
                }
            }
        });
    }    
});

El content que se injecta en la página actual, dentro del diálogo, podría ser algo como esto:


 

Hola, soy el contenido

//

El problema surge porque jQuery añade algo en la carga del script “interior”. En lugar de cargarse como:

/foo/bar/js/another-library.js

se carga como

/foo/bar/js/another-library.js?_=1370596993170

*El valor numérico es aleatorio y cambia cada vez.

No hace lo mismo con los ficheros CSS, que sí entran en el proceso de cacheo del navegador, aunque sapnsLoader está preparado para hacer lo mismo con fichero CSS. Simplemente indicamos un parámetro más en la carga.

De esta manera, jQuery fuerza a que dichos recursos no se cacheen. Se incluye un parámetro _ (guión bajo) con un valor aleatorio que disuade al navegador de calcular un cacheo. Como la URL no es igual siempre la carga desde la ubicación indicada.

¡sapnsLoader (basket.js) al rescate!

Para solucionarlo utilizamos nuestro propio “cargador de recursos”:


 

Hola, soy el contenido

//

Con esta solución conseguimos dos cosas:

  1. Evitamos el problema de las librerías forzosamente-no-cacheadas que he contado antes.
  2. Facilitamos la implementación de estos pedazos de HTML+CSS+JS, ya que en cada uno de ellos sólo tenemos que pensar en las librerías JavaScript que necesitamos. No tenemos que pensar si las hemos cargado antes (porque en la página padre eran necesarias). sapnsLoader se encarga de esa tarea por nosotros.

Conclusiones:

Finalmente podemos añadir que sapnsLoader sería capaz de funcionar con navegadores que no tuvieran localStorage. En ese caso cargaría (desde su URL) las librerías siempre, salvo que ya existieran en la cabecera.

También podemos decir que podríamos tener un problema de almacenamiento si abusáramos del localStorage. Como se ha visto más arriba la capacidad varía entre 2.5 MB y 10 MB. Si utilizaramos localStorage para “otras cosas” habría que ver, el caso concreto, y controlar que, quizá, necesitemos más espacio.

Si pensamos sólo en sapnsLoader es muy poco probable que tengamos problemas de espacio. Los recursos que estamos cargando (que normalmente están minimizados) ocupan unos pocos KBs. Tendríamos que cargar muchas librerías para saturar dicho espacio.

En camino hay una solución más potente que localStorage pero que todavía no está totalmente implementada ni estable: IndexedDB. Sigue una filosofía similar a localStorage (utiliza el sistema de almacenamiento clave/valor) pero con una API más extensa y que tiene como objetivo almacenar una mayor cantidad de información (mucho más que localStorage).