Usando Sprites CSS para disminuir el número de peticiones

sprite css

Mejorar la velocidad de carga de la web debería ser un objetivo prioritario. En un tema anterior hablé sobre optimizar la carga de los recursos. Un forma de llevar un seguimiento acerca de esta materia es usando PageSpeed. Tras minimizar el tamaño de los documentos ahora llevo a cabo la combinación de imágenes en objetos móviles CSS conocidos generalmente como sprites CSS.

En la gráfica adjunta puede ver la evolución de la página de inicio de este sitio desde que la publiqué por primera vez el 16 de Mayo de 2010 hasta el pasado 7 de octubre de 2012. Cada fecha representa un momento de un cambio importante en los contenidos de mi página principal. En azul se representa el tamaño total de los recursos, es decir, HTML, JS, CSS y XHR. En rojo está el número de peticiones que hace el navegador solicitando esos recursos. Empecé con menos de 100 KB y unas 20 peticiones. A partir de cierta fecha incorporé imágenes con el texto en esa página de inicio por lo que ambos valores empezaron a crecer. El 8 de julio de 2012 minimicé los documentos HTML, JS y CSS, observándose como con un número similar de recursos (peticiones) y de contenidos, el tamaño bajó desde 363 KB a 255 KB (casi 30% de reducción). El pasado 7 de octubre implementé los Sprites CSS y el número de peticiones bajó desde 42 hasta 29 (reducción 31%) a pesar de mantener un contenido y tamaños similares entre ambas publicaciones. Reducir las peticiones es un aspecto a tener en cuenta, pues por muy pequeño que sea el recurso el navegador debe solicitarlo, bien al servidor o extraerlo desde una caché.

Una forma de reducir el número de peticiones es combinar varias imágenes en estos Sprites CSS. En este documento de Google Developers Combine images using CSS sprites nos habla sobre este tema. Podemos resumir lo siguiente:

  • Se trata de combinar imágenes que son cargadas siempre en una o todas las páginas, como conjuntos de iconos o imágenes para botones.
  • Deberían ser imágenes de pequeño tamaño, aquellas donde el navegador puede invertir un tiempo considerable en relación con el tamaño de la imagen.
  • En caso de tener declarados los tiempos de vida de caché de las imágenes, es obvio que serían candidatas las de mayor tiempo de vida. En definitiva, las imágenes no deben cambiar frecuentemente.

Por ejemplo, la imagen del logo del sitio es un candidato, pues suele aparecer en todas las páginas, es de pequeño tamaño y no cambia con frecuencia. No es díficil elegir las imágenes candidatas para un Sprite CSS. Quizás es más complejo generar el CSS aunque hay muchas herramientas por ahí que pueden servir. El documento anterior de Google Developers nos da un enlace a una de ellas.

Usando el compositor de imágenes como generador de sprites

Un sprite de ejemplo

La herramienta que hace poco publiqué compositor de imágenes y generador de Sprites CSS puede también servir para construir estos sprites. En esta imagen puede ver un trozo del sprite general para este sitio, con el fondo transparente resaltado tal como lo muestra ese compositor de imágenes. La imagen real está en este archivo sprite.png. El CSS generado lo he puesto en el archivo de estilo cabeza-pie.css. Los cambios en el HTML se explicarán en el último apartado. Pero empecemos por el principio. Los pasos a seguir para crear un Sprite CSS con la herramienta compositora de imágenes son:

  1. Elegir los archivos de imágenes que desee incluir insertándolos en el área contenedora. Puede también arrastrar una imagen desde una carpeta de su ordenador.
  2. En la pestaña edición puede alinear horizontal o verticalmente las imágenes, ajustando el contenedor tras alinear. Las imágenes se ordenan por tamaños con objeto de ocupar la menor superfice posible.
  3. Luego puede extraer la composición a un formato de texto. Copiando este texto y salvándolo en un archivo de texto en su ordenador le permitirá en un momento posterior volver a cargar esa composición y, en su caso, realizar modificaciones agregando o eliminando imágenes de la composición.
  4. Ha de obtener la imagen del sprite. Con el menú del navegador debe guardar esta imagen en el lugar definitivo del sitio. El nombre del archivo deberá ser igual al que se declara en la siguiente parte de generación de CSS (ver nota siguiente).
  5. Por último ha de generar el CSS teniendo en cuenta que:
    • Se ha de señalar una carpeta donde se encontrará la imagen del sprite, es decir, la carpeta donde guardó la imagen en el paso anterior.
    • Debe dar un nombre al sprite, pues en su sitio puede tener varios sprites diferentes. Este nombre debe ser igual que el del archivo de la imagen que guardó en el paso anterior. (ver nota siguiente)
    • Tiene que seleccionar alguna de estas dos opciones o las dos:
      • Generar selectores tipo class, que serán de la forma .abc {...}
      • Y/O generar selectores tipo data-X-src, que serán de la forma *[data-X-src="..."]
    • La opción background-image activada inserta la imagen de fondo en cada declaración CSS. Desactivada crea una declaración sólo para el fondo de imagen.

No es necesario que el nombre del archivo de imagen sea el mismo que el nombre del sprite elegido. Especialmente si en un momento posterior necesitamos modificar el Sprite, agregándole una marca de tiempo al nombre del archivo para evitar la caché del navegador. He agregado un nuevo apartado a este tema acerca de cómo modificar Sprites CSS.

Aunque no es necesario que el nombre del archivo sea igual al del selector del Sprite, es recomendable que podamos identificar los selectores con el Sprite de donde proceden, sobre todo cuando tenemos más de un Sprite CSS en nuestro sitio.

Sprites CSS en elementos de bloque

Sprite de demostración

Veámos un ejemplo simple. Con la aplicación anterior he compuesto tres imágenes usando una de 61×23 píxeles cuyo nombre de archivo es twitter-a.png, otra del mismo tamaño 61×23 píxeles del archivo facebook-a.png y la tercera de 32×32 píxeles para googleplus-a.png. Con esa herramienta las he alineado horizontalmente usando también la opción de ajustar el contenedor. He guardado el sprite con el nombre demo.png con el aspecto que muestra la imagen junto a este párrafo. Con la opción únicamente activada de selectores class generaríamos este CSS:

*[data-demo-src]{
    background: url('/como-se-hace/sprites-css/ejemplos/demo.png') no-repeat top left;
    }
.twitter-a-png{
    background-position: -99px -2px;
    width: 61px;
    height: 23px;
    }
.facebook-a-png{
    background-position: -36px -2px;
    width: 61px;
    height: 23px;
    }
.googleplus-a-png{
    background-position: -2px -2px;
    width: 32px;
    height: 32px;
    }

Se observa el primer selector *[data-demo-src] que declara el fondo de imagen para todos los elementos que porten un atributo data-demo-src. Acerca del posicionamiento del fondo puede consultar Fondos en CSS-3 {background} y también Fondos en CSS-2.1 {background}. El background-position es realmente la posición izquierda y arriba de cada imagen en la composición. Así la imagen googleplus-a-png se encuentra en (2,2) píxeles (al realizar la composición he dejado un margen de 2 píxeles entre imágenes). De esta forma el navegador desplaza la imagen (-2,-2) píxeles presentándola desplazada. El estilo width y height que se aplicará al elemento determina exactamente el trozo de la imagen total que será presentada y que coincide con googleplus-a-png. Tras poner el CSS anterior en un elemento <style> de la cabecera de esta página, a continuación insertamos estas tres imágenes desde el sprite usando elementos <div>

Ejemplo:

 

Como son elementos de bloque, se disponen visualmente cada uno en una línea, pues después de renderizar un elemento de bloque, el navegador insertará un salto de línea. El HTML usado para este ejemplo es el siguiente:

<div class="twitter-a-png" data-demo-src>&nbsp;</div>
<div class="facebook-a-png" data-demo-src>&nbsp;</div>
<div class="googleplus-a-png" data-demo-src>&nbsp;</div>

Puede parecer que tener que poner el atributo data-demo-src en cada elemento que necesitemos es una pérdida de tiempo. Para evitarlo habría que incorporar la imagen en cada selector:

.twitter-a-png{
    background: url('/como-se-hace/sprites-css/ejemplos/demo.png') no-repeat top left;
    background-position: -99px -2px;
    width: 61px;
    height: 23px;
    }
    ...
    

Aunque creo que hacerlo así ocupará más espacio en el CSS sin aportar ninguna ventaja. Además ese atributo en el HTML nos indicará rápidamente que es un sprite e incluso podemos saber su nombre. Podríamos tener varias composiciones de sprites para una página, como de hecho sucede en esta misma página.

Sprites CSS en elementos de línea

¿Qué elementos HTML podemos usar como sprites?. Antes puse un ejemplo con elementos <div>. Éste es un elemento de bloque y podemos definir con CSS sus dimensiones width, height. ¿Y si necesitamos la imagen en un elemento de línea como <span>?. En estos elementos las dimensiones vienen dadas por el contenido, generalmente de texto. Así si ponemos el sprite en elementos <span> nos queda algo incompleto como esto:

Ejemplo:

 

Esto lo hemos hecho con este código:

<span class="twitter-a-png" data-demo-src>&nbsp;</span>
<span class="facebook-a-png" data-demo-src>&nbsp;</span>
<span class="googleplus-a-png" data-demo-src>&nbsp;</span>

La altura de este elemento de línea (o a nivel de línea o de tipo inline) viene determinada por la altura de fuente, especialmente la propiedad font-size aunque también line-height que define la altura de línea. El ancho del <span> es el del texto interior. En este caso el ancho es el del texto &nbsp;, un espacio no separable cuya longitud apenas cubre unos pocos píxeles de ancho. El elemento ignora completamente el width y height del CSS.

Esto lo podemos resolver forzando el elemento de línea a bloque con la propiedad display y con el valor inline-block como hacemos a continuación con los anteriores elementos <span>:

Ejemplo:

 

Este es el código:

<span class="twitter-a-png" data-demo-src
style="display: inline-block;">&nbsp;</span>
<span class="facebook-a-png" data-demo-src
style="display: inline-block;">&nbsp;</span>
<span class="googleplus-a-png" data-demo-src
style="display: inline-block;">&nbsp;</span>

Así el elemento recoge las propiedades width y height como si fuera de bloque, pero si su contenido cabe en una línea no genera un salto de línea. Aunque vea {display} con valor inline-block para más información.

Este ejemplo y el anterior usan el atributo class para incoporar el estilo del sprite. Esto supone una limitación puesto que es posible que todos los elementos que incorporen sprites no tengan el mismo estilo restante. E incluso si lo hacemos sobre páginas ya construidas, los elementos ya podrían tener su clase y tendríamos que estarlas buscando e incorporarles el nuevo estilo del fondo. Esto no es eficiente a la hora de hacer modificaciones en el sprite para agregar, eliminar o modificar alguna imagen de la colección. Si tenemos todo el CSS del sprite agrupado e independiente solo restaría regenerar el CSS nuevo y sobrescribir el que ya tengamos. Aunque las posiciones de cada imagen sean diferentes tras el cambio, estas sólo están declaradas en el CSS en un sólo sitio y no habría que hacer más nada en otras partes del documento CSS o HTML. Los elementos se readaptarían a la nueva composición del sprite.

En resumen, hemos de buscar una alternativa para dotar el estilo sin tener que usar class. Una forma es seleccionar por un atributo data-* (puede ver más información sobre los selectores CSS). En el apartado siguiente se expone la opción data-X-src del compositor de imágenes, aunque la usaremos en elementos <img> pero podría utilizarse con cualquier elemento.

Sprites CSS con elementos de imagen

Un elemento reemplazado es, típicamente, un elemento <img>. Éste también es un elemento de línea, como el <span>, pero sus dimensiones vienen impuestas por las que íntrinsecamente posee el elemento. Así <img> tiene los atributos width y height que, si no se declaran, serán definidos con los valores que se obtengan del ancho y alto de la imagen cuando sea cargada. Dicho sea de paso, esto es un motivo por el que siempre debemos declarar estos atributos en un <img>, pues de otra forma el navegador no puede renderizar adecuadamente la página mientras no se cargue la imagen dado que no sabe que dimensiones va a tener.

Además si especificamos propiedades CSS para width y height el navegador sí hará caso aún cuando se trata de un elemento de línea. Estas nuevas propiedades CSS sobrescribirán a los atributos HTML que se hayan declarado y/o obtenido al cargar la imagen. Volvemos a reproducir el ejemplo usando ahora elementos <img>

Ejemplo:

blanco

El código es el siguiente:

<img src="/res/img/blanco.gif" width="2" height="2" alt="blanco"
    class="twitter-a-png" data-demo-src />
<img src="/res/img/blanco.gif" width="2" height="2" alt="blanco"
    class="facebook-a-png" data-demo-src />
<img src="/res/img/blanco.gif" width="2" height="2" alt="blanco"
    class="googleplus-a-png" data-demo-src />

Developer tools Chrome

Como es un elemento imagen requiere los atributos src, width, height y alt. Usamos la imagen blanco.gif que es una transparente de 2×2 píxeles. Incluso especificando el ancho y alto vemos que luego el navegador ajusta el tamaño al declarado en el width y height del CSS, como se observa en esta captura de pantalla del Developer Tools de Chrome. Para ser más eficientes dado que siempre conviene que el navegador conozca desde el principio las dimensiones finales de un elemento <img>, deberíamos poner las dimensiones CSS en el width y height del elemento.

Además hay otra razón para utilizar <img> como contenedores de sprite CSS. Si ya tenemos un sitio montado como este wextensible.com, hay un montón de pequeñas imágenes a modo de icono que hemos de convertir en sprites CSS. Y esto se puede hacer con alguna herramienta que permita reemplazar código HTML en múltiples documentos. De hecho esa herramienta ya la he construido y con ella he convertido todas las imágenes pertinentes de este sitio en sprites CSS. Siguiendo con el ejemplo anterior generamos el CSS con la opción data-X-src, aunque le cambiaremos el nombre del sprite a demo2:

*[data-demo2-src]{
    background: url('/como-se-hace/sprites-css/ejemplos/demo.png') no-repeat top left;
    }
*[data-demo2-src="/res/img/twitter-a.png"]{
    background-position: -99px -2px;
    width: 61px;
    height: 23px;
    }
*[data-demo2-src="/res/img/facebook-a.png"]{
    background-position: -36px -2px;
    width: 61px;
    height: 23px;
    }
*[data-demo2-src="/res/img/googleplus-a.png"]{
    background-position: -2px -2px;
    width: 32px;
    height: 32px;
    }

Como en el ejemplo que usábamos class, ahora también tenemos un selector general para poner el fondo en todos los elementos que porten el atributo data-demo2-src, apuntando a la misma imagen de fondo que el primer ejemplo de nombre demo. Pero ahora incorporamos cada imagen con un selector por ese atributo que precisamente va a contener la ruta que antes tenía el elemento imagen en su atributo src. Por ejemplo, si antes teníamos una imagen <img src="xxx.gif" width="nn" height="mm" ... /> tras la conversión vamos a tener <img src="blanco.gif" data-demo-src="xxx.gif" width="nn" height="mm" ... />, donde blanco.gif es cualquier imagen transparente para poder ver el fondo que se incopora con CSS. El ejemplo anterior en ejecución quedaría ahora así:

Ejemplo:

twitter facebook googleplus

Cuyo código es este:

<img src="/res/img/blanco.gif" data-demo2-src="/res/img/twitter-a.png"
    width="61" height="23" alt="twitter" />
<img src="/res/img/blanco.gif" data-demo2-src="/res/img/facebook-a.png"
    width="61" height="23" alt="facebook" />
<img src="/res/img/blanco.gif" data-demo2-src="/res/img/googleplus-a.png"
    width="32" height="32" alt="googleplus" />

Esta forma de proceder tiene las siguientes ventajas:

  1. Permite crear un procedimiento automático que busque los elementos <img> y cambiar su atributo src="xxx" por data-demo-src="xxx", añadiendo luego un nuevo atributo src="yyy" con la imagen transparente. El resto no hay que tocarlo, pues ya debería tener el width y height correcto.
  2. No utiliza el atributo class quedando este libre para otros cometidos.
  3. No perdemos la referencia a la imagen original con la que compusimos el sprite. Su ruta sigue estando, como se ve en el data-demo2-src. Además la ruta de la imagen original es un indentificador único por lo que no se prestará a confusiones. Ante cualquier incidencia siempre podemos volver a cargar la composición original del sprite y buscar esa imagen para arreglar algo.
  4. Deshacer algo es sencillo, basta hacer lo contrario del primer punto y volvemos a tener el elemento imagen original.

En cuanto al primer punto, la aplicación compositora de imágenes también nos da un listado de rutas que componen el sprite. Esto nos puede servir para alimentar esa herramienta que busque esos elementos <img> y realizar el cambio genérico en todos los documentos.

Modificando Sprites CSS

Sprite CSS

Inicialmente puse el nombre de archivo sprite.png a la imagen del Sprite CSS general que uso en este sitio. Al mismo tiempo en la herramienta compositora de Sprites le puse también ese nombre para construir el selector CSS generado de la forma data-sprite-src. Pero hace poco agregué algunas imágenes al Sprite usando la herramienta compositora de imágenes. Son los nuevos iconos      . Luego generé la imagen y la guardé con el mismo nombre sprite.png y volví a generar el CSS actualizándolo en el sitio. Cuando ejecuté esto observé que el navegador había puesto en caché la imagen anterior sprite.png y por lo tanto el CSS no rescataba las nuevas posiciones con esa imagen. Tras analizarlo observé que no es necesario que el nombre de la imagen sea igual que el del selector data-X-src del Sprite. Y por otro lado que debería añadir una marca de tiempo al nombre del archivo para evitar la caché del navegador. Por lo tanto tras generar el CSS le incorporé esta marca de tiempo en formato día-mes-año-horas-minutos para forzar al navegador a solicitar esa nueva imagen. Modificando así el nombre del archivo sólo hay que ir al CSS y agregarlo al selector general que declara el background con la imagen. Con el resto de selectores no hay que hacer nada. En el código siguiente puede ver como quedó el CSS del Sprite de este sitio (puede ver el actual en el archivo cabeza-pie.css):

/* Para evitar cache poner fecha-hora a la imagen sprite */
    *[data-sprite-src]{
        background: url('/res/img/sprite-18-10-2012-20-19.png') no-repeat top left;
    }
    *[data-sprite-src="/res/img/alargador.png"]{
        background-position: -128px -147px;
        width: 16px;
        height: 16px;
        }
    *[data-sprite-src="/res/img/facebook-b.png"]{
        background-position: -110px -147px;
        width: 16px;
        height: 16px;
        }
    ...(El resto permanece igual)

Aunque no es necesario que el nombre del archivo de imagen sea igual al del selector del Sprite, es recomendable que podamos identificar los selectores con el Sprite de donde proceden, sobre todo cuando tenemos más de un Sprite CSS en nuestro sitio. Respetar esto es una ventaja, pues si vemos en cualquier elemento el atributo data-xxx-src podemos saber que pertenecerá a la imagen de sprite como xxx.png, o bien xxx-marca_de_tiempo.png si ya la hemos modificado con la marca de tiempo.