Mejorando la carga de un sitio web: Imágenes vs. velocidad

blog snack performance

Actualmente las imágenes son un recurso muy importante para cualquier sitio web, mejoran la interfaz y ayudan a transmitir información de manera visual (sin olvidar los memes). Lamentablemente la mayoría de sitios web no tienen en cuenta lo perjudiciales que puede ser.

Con la gran cantidad de dispositivos que actualmente pueden acceder a un sitio web, hay que tener muchas cosas en consideración: tamaño de pantalla, conexión, DPI, tipo de dispositivo, etc. Logrando generar la "imagen perfecta" dependiendo el tipo de dispositivo. El resultado es una carga más rápida para el sitio, una mejor experiencia para el usuario y disminuyendo el consumo de datos.

Tipos de formato

Existe una gran variedad de formatos que hoy puedes usar, pero ¿realmente estás usando el correcto?, puede que no te parezca importante, la realidad es que esto marca una enorme diferencia en el peso, color, uso y calidad de tu imagen.

Formato Categoría Colores Compatibilidad (navegador)
JPEG Lossy (pérdida) 16 millones Todos
PNG Lossless (sin pérdida) Máximo 256 o ilimitados Todos
GIF Lossless Máximo 256 Todos
JPEG XR Lossy y lossless 281 billones IE, Edge
SVG Lossless ilimitados Todos (a partir de ciertas versiones)
WebP Lossy y lossless 16 millones Chrome, Opera, Android
Oye oye, despacio cerebrito

Bueno, bueno, tú tranquilo, ¿Qué significa eso de la categoría y los colores?, y ¿qué son esos formatos extraños? te lo explico ahora paso por paso.

Lossy y lossless

Lossy y lossless son dos términos que se usan en la compresión de un archivo. Con lossless cada bit de datos que estaba originalmente en el archivo permanece intacto. Por otro lado, lossy reduce un archivo al eliminar permanentemente cierta información (en imágenes los usuarios no ven dicha pérdida).

Con esto podemos decir que una imagen JPG no va a ser igual a una PNG, puesto que tienen una diferente compresión y por lo tanto esto implica un peso distinto.

Colores

Para explicar el tema de los colores primero te hablaré sobre la profundidad de color, que vendría siendo el número de bits utilizados para indicar el color de un píxel en una imagen o video, es decir, la cantidad de bits que hay en un solo píxel.

Cuantos más bits se usen, mayor será la cantidad de colores, la calidad y el peso de la imagen también se vera afectada.

  • 1 bit: 2 a la 1  = 2 colores
  • 2 bits: 2 a la 2 = 4 colores
  • 4 bits: 2 a la 3 = 8 colores
  • 8 bits: 2 a la 8 = 256 colores (PNG-8, GIF)
  • 24 bits: 2 a la 24 = 16,777,216 colores (JPEG, WebP)
  • 48 bits: 2 a la 48 = 281,474,976,710,656 (JPEG XR)

JPG

Es un estándar de compresión y el formato más usado para las imágenes. La compresión va desde el 0 % hasta el 100 % (generalmente del 60 % al 75 %), pero al ser lossy siempre habrá pérdida lo cual no es muy útil para la edición continua.

Nota

Uso: Imágenes fijas, fotografías, imágenes con colores complejos y dinámicos.

PNG

Un formato que vendría siendo la fusión de GIF y JPEG por sus dos variantes:

PNG-8

El formato PNG-8 está limitado a una paleta de 256 colores como máximo, tiene mejores opciones de trasparencia que un GIF (si, los GIFs tienen transparencia) y el peso es ligeramente menor.

PNG-24

El formato PNG-24 representa imágenes con millones de colores, al igual que JPEG, la diferencia es que este preserva la transparencia. Al ser lossless el peso de la imagen va a ser mayor, pero si la calidad es más importante para ti, este es tu formato.

Nota

Uso: Gráficos con transparencia, fotografías y gráficos con colores pesados y complejos, imágenes que necesiten de edición.

GIF

Un formato limitado a un máximo de 256 colores con la capacidad de tener transparencia, útil para imágenes en las que no se requiera calidad, ya que es lossy.

Nota

Uso: Animaciones simples, gráficos con baja variación de píxel (colores planos).

JPEG XR

Es un formato creado y usado por Microsoft, ofreciendo una mayor calidad en menor tamaño que el formato JPEG, incluso hasta alcanzar el 50 %, digamos que es el HD de un JPEG (se le suele llamar así). Lo mejor de este formato es que cuenta con la posibilidad de guardar con pérdida o sin pérdida (lossy o lossless), básicamente tenemos una opción más en el JPEG, también cuenta con más colores que un JPEG, tiene 48-bits (281 billones), lo cual nos da una mejor calidad.

Todo esto suena muy cool pero para web solo sería compatible con IE y Edge (quién lo diría, es el día opuesto)

Nota

Uso: Los mismos que JPEG, con la variante de que tenemos lossy, lossless y un número mucho mayor de colores.

SVG

A excepción de los otros formatos, un SVG no es un mapa de bits, sino que se trata de un formato vectorial. Pueden ser creados en aplicaciones de gráficos vectoriales como Illustrator o Sketch y gracias a que está basado en XML, se puede editar en cualquier editor de texto y ser modificado por lenguajes como CSS o JavaScript. Al ser vectores estos se pueden escalar a cualquier tamaño sin sufrir perdida.

Nota

Uso: Logotipos e iconos, gráficos de múltiples tamaños y pantallas, gráficos que responden a un dispositivo, gráficos que deben editarse o actualizarse (como mencione, estos pueden ser manipulados con CSS o JavaScript).

WebP

Es un formato que soporta guardar con pérdida o sin pérdida (similar a JPEG XR), con la diferencia de que ahí tenemos 24-bits (igual que un JPEG). Pretende ser un nuevo estándar abierto para las imágenes, compitiendo así con JPEG proporcionando un menor peso para las imágenes con una calidad comparable, por cierto, esta siendo desarrollado por Google.

Nota

Uso: Los mismos que JPEG, con la variante de que tenemos lossy y lossless.

Ya que conoces los diferentes formatos que hay para web (realmente hay más, pero estos son los importantes) necesito que te hagas una pregunta.

¿Realmente necesitas usar una imagen?

Sé que es una pregunta extraña después de toda la carga de información que haz visto, pero muchas veces preferimos que algo se vea bonito en vez de que sea funcional.

La mejor imagen es la que no usas - @dezkareid

Esto me lleva a otro punto, ¿deberíamos usar imágenes o es que hay otra posibilidad?, bueno, pues puede que la haya. Conozcamos las fuentes de iconos y base64

Fuentes de iconos

Las fuentes de iconos son, valga la redundancia, fuentes, sin embargo, estas no contienen letras o números, contienen símbolos y glifos. La gran ventaja es que pueden ser manipuladas con CSS, como cualquier fuente.

.icono {
  font-size: 50px;
  color: blue;
  opacity: 0.8;
  transition: 0.3s;
}

.icono:hover {
  opacity: 1;
  color: red;
  transform: scale(1.1);
}

Ventajas

  • Aplicar cualquier propiedad de CSS
  • Al ser vectores, no hay que preocuparnos por la calidad
  • Se puede enviar una sola solicitud HTTP para todos los iconos
  • Compatible con todos los navegadores, incluso IE6

Desventajas

  • No funcionan para imágenes complejas, solo iconos simples
  • Suelen estar limitados a un solo color, a menos que se apliquen trucos CSS

FontAwesome

FontAwesome es una amplia colección de varios iconos servidos por CDN (también se puede descargar).

Solo basta con agregar la biblioteca a tu proyecto y con el uso de clases en tu HTML obtienes tu icono.

<link rel="stylesheet" 
      href="https://use.fontawesome.com/releases/v5.3.1/css/all.css"
      integrity="sha384-mzrmE5qonljUremFsqc01SB46JvROS7bZs3IO2EmfFsd15uHvIt+Y8vEf7N7fWAU"
      crossorigin="anonymous">

<i class="fas fa-bars"></i>
See the Pen Font Awesome - ejemplo by Fili Santillán 👾 (@filiJS) on CodePen.

IcoMoon

IcoMoon funciona igual que FontAwesome, pero este cuenta con muchas más capas de personalización y manipulación de los iconos, cuentas con más formatos (SVG, PNG, PSD, AI) de descarga y bibliotecas de iconos, de pago y gratuitas (incluso tiene a FontAwesome), la gran ventaja y diferencia es que puedes importar tus propios iconos SVG.

Personalización de iconos

Te recomiendo ver los documentos si quieres usar icoMoon en tus proyectos.

Base64

Base64 es una forma de codificar datos binarios en un conjunto de caracteres ASCII con el fin de transmitir los datos sin pérdida o modificación. Cuando se tienen datos binarios que se desean enviar, generalmente no se hace directamente porque algunos medios (como los correos) están hechos para transmitir y recibir texto, entonces si envías datos binarios, lo más probable es que no se interpreten como deberían o incluso se podrían corromper.

Los navegadores actuales e IE8 en adelante pueden entender base64 usando Data URIs, un método de incrustación de imágenes y archivos en páginas web usando cadena de texto, esto quiere decir que podemos transformar una imagen a base64 e insertarlo en un sitio web.

See the Pen Base64 - Ejemplo by Fili Santillán 👾 (@filiJS) on CodePen.

La gran ventaja es que ya no se haría una petición HTTP para traer una imagen, todo estaría en el archivo. ¿Suena genial no?, bueno la mala noticia es que tu archivo va a ser más pesado.

Supongamos que tienes un archivo HTML con 50 imágenes (<img>), que vendrían siendo 51 peticiones para tu sitio web. Esto significaría bastante tiempo de carga. En cambio, con base64 podrías tener esas 50 imágenes dentro de tu archivo HTML y sí, tu archivo HTML va a ser mucho más pesado, pero eso se compensa al ser tan solo una petición. Solo ten cuidado de que tus archivos no sean tan pesados, puede ser contraproducente si conviertes todas tus imágenes.

Mi recomendación es usar base64 para imágenes que no excedan los 100 KB y no abusar del número de imágenes. Para facilitar este proceso hago uso de webpack con el loader webpack-url-loader.

Nota

Si usas Gulp o Grunt, existen varias opciones: Para Gulp Lovers, Parara Grunt Lovers.

Base64 con Webpack

Como dije, yo uso webpack-url-loader. Un loader para que transforma los archivos en URIs base64.

Advertencia

Este mini tutorial es asumiendo que tu ya sabes usar Webpack.

Instalación

#npm
$ npm install url-loader --save-dev

#yarn
$ yarn add url-loader --dev

Configuración

Antes de empezar a transformar, es importante especificar los tipos de archivos que se van a tomar en cuenta y el peso máximo, si algún archivo rebasa ese límite, no va a aplicar la codificación.

module.exports = {
  module: {
    rules: [
      {
        test: /\.(jpg|png|gif)$/, // Formatos a convertir
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 100000 // Se aplica si el archivo es más pequeño que 100000 bytes (100KB)
            }
          }
        ]
      }
    ]
  }
}

Uso

Ya que tengas lista tu configuración, hay que usar javascript para mostrar las imágenes.

// Importamos la imagen
import myImage from "./ruta/de/tu/imagen.jpg"

// Dibujamos la imagen
const myImage = document.createElement("img");
img.setAttribute("src", myImage);

document.element.append(img); // "element" representaría a un elemento HTML (body, div, section, article, etc.)

En CSS

body {
  background-image: url(./ruta/de/tu/imagen.jpg); 
}

¿Qué pasa con las imágenes que rebasan el tamaño?

Podemos definir un fallback, como file-loader. Si url-loader se encuentra con una imagen mayor a 100KB, se usa file-loader y la imagen se manejaría como una petición normal.

{
  test: /\.(jpg|png|gif)$/,
    use: {
      loader: "url-loader",
      options: {
        limit: 100000,
        fallback: "file-loader"
      }
    }
}

Otros archivos

Aparte de imágenes con url-loader también tenemos la posibilidad de codificar otro tipo de archivos.

{
  test: /\.(jpg|png|gif|woff|eot|ttf|svg|mp4|webm)$/,
}

Tamaños y adaptaciones

El mayor problema que suelo ver en los sitios web respecto a las imágenes, es el incorrecto uso de tamaños y adaptaciones, es decir, que se usa una imagen con un tamaño (menor o mayor) a como se ve en el sitio.

Ejemplo, digamos que tenemos una etiqueta img con una imagen de 500 x 500px, pero con CSS modificamos el tamaño a 300 x 300px.

En la izquierda la imagen original (500 x 500), a la derecha la imagen modificada con CSS (300 x 300)

Adicionalmente ten en cuenta que tus usuarios pueden entrar desde múltiples dispositivos y estás usando la misma imagen.

Misma imagen para distintos tipos de pantalla

No hay que olvidar los distintos tipos de pantalla (pantallas de alta resolución).

Sé que parece complejo todo este tema, pero la solución es muy sencilla y lo puedes aplicar con HTML y CSS, nada de librerías o trucos raros.

Imágenes con HTML

srset:

Ver soporte

El atributo srcset permite definir varios recursos de imagen que ayudan a determinar la fuente apropiada. Usemos un ejemplo un una etiqueta <img> y como responde está ante distintas pantallas.

<img src="image.jpg">

En pantallas de baja resolución se va a ver bien, pero en pantallas de alta resolución (la mayoría de dispositivos móviles) se van a ver mal.

Ahora agreguemos el atributo srcset y veamos como se comporta.

<img src="image.jpg" srcset="image@2x.jpg 2x, image@3x.jpg 3x">

El @2x.jpg o @3x.jpg solo es una forma de denotar variantes de imágenes de alta resolución. Para pantallas con resolución estándar se usa image.jpg, para pantallas con mayor resolución image@2x.jpg y pantallas con aún mayor resolución se usa image@3x.jpg.

Cuanto más alta sea la resolución, mayor será la densidad de píxeles de la pantalla. Como resultado, las pantallas de alta resolución requieren imágenes con más píxeles.

Imagen original en el sitio de Apple

¿De qué tamaño deberían ser tus imágenes?, solo hay que aumentar el tamaño original al doble y triple.

  • image.jpg (original) = 720 x 500px
  • image@2x.jpg = 1440 x 1000px
  • image@3x.jpg = 2160 x 1500px
Nota

Dependiendo del tipo de pantalla, vas a ver la imagen adecuada.

Nota

srcset se puede extender aún más y usarse para responder dependiendo el tamaño de pantalla, pero para eso usaremos otra técnica.

Tip

Para mejores resultados, te recomiendo leer A Beeter Way To Design For Retina In Photoshop donde se aplican otras técnicas para diseñar en retina..

picture:

Ver soporte

El elemento <picture> permite definir varias fuentes para controlar la imagen que se va a mostrar en distintos escenarios.

<picture>
  <source srcset="image-468.jpg" media="(min-width: 468px)">
  <source srcset="image-600.jpg" media="(min-width: 600px)">
  <source srcset="image-1024.jpg" media="(min-width: 1024px)">
  <img src="image.jpg" alt="Image">
</picture>

<picture> hace uso de srcset para especificar la imagen, también contamos con un nuevo atributo media que funciona de igual manera que los Media Queries en CSS.

En el código estamos diciendo que para dispositivos con el ancho mínimo de 468px use la imagen image-468.jpg, si el ancho mínimo es 600px se usa image-600.jpg, si el ancho mínimo es 1024px se usa image-1024.jpg y si no se llega a cumplir ninguna de esas reglas, se usa lo que hay en la etiqueta img.

Por si lo llegaste a pensar, si, también se puede hacer uso de srcset para determinar la imagen correcta, dependiendo el tipo de pantalla.

<picture>
  <source srcset="image-468.jpg 1x, image-468@2x.jpg 2x" media="(min-width: 468px)">
  <source srcset="image-600.jpg 1x, image-600@2x.jpg 2x" media="(min-width: 600px)">
  <source srcset="image-1024.jpg 1x, image-1024@2x.jpg 2x" media="(min-width: 1024px)">
  <img src="image.jpg" srcset="image@2x.jpg 2x" alt="Image">
</picture>

Otra función que podrías usar para <picture> es el cargar el tipo de formato correcto, dependiendo del navegador que se está usando.

<picture>
  <source srcset="image.webp" type="image/webp">
  <source srcset="image.jxr" type="image/vnd.ms-photo">
  <img src="image.jpg" alt="fallback">
</picture>

Imágenes con CSS

En CSS es distinto y mucho más fácil, gracias a los Media Queries podemos una imagen dependiendo el tamaño de la pantalla.

.element {
  background-image: url(image.jpg);
}

@media (min-width: 468px) {
  .element {
    background-image: url(image-468.jpg);
  }
}

@media (min-width: 600px) {
  .element {
    background-image: url(image-600.jpg);
  }
}

Idéntico a <picture>, aunque podemos decir que <picture> fue quien se basó en los Media Queries de CSS.

Puede que ahora te estés preguntando ¿y que pasa con el tipo de resolución de pantalla?, tranquilo, CSS tiene solución a ello.

Media Queries: Resolution

Ver soporte

Volvemos a hacer uso de nuestros amigos los Media Queries, solo que esta vez para un propósito totalmente distinto.

.element {
  background-image: url(image.jpg);
}

/* Consultamos si la pantalla es de alta resolución */
@media (min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
  .element {
    background-image: url(image@2x.jpg);
  }
}

/* Lo mismo aquí*/
@media (min-device-pixel-ratio: 3), (min-resolution: 288dpi){
  .element {
    background-image: url(image@3x.jpg);
  }
}
Nota

Es necesario contar con min-device-pixel-ratio y min-resolution para que la regla se aplique.

Si tienes duda sobre cuantos dpi (pixeles por pulgada) debes de usar:

  • 1 dpi = 96
  • 2 dpi: 96*2 = 192
  • 3 dpi: 96*3 = 288

Finalmente, solo queda mezclar las reglas necesarias para servir la imagen correcta.

/* Imagen para dispositivos de baja resolución */
.element {
  background-image: url(image.jpg);
}

/* Imagen para dispositivos de alta resolución */
@media (min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
  .element {
    background-image: url(image@2x.jpg);
  }
}

/* Imagen para dispositivos de baja resolución y el ancho mínimo de 468px */
@media (min-width: 468px) {
  .element {
    background-image: url(image-468.jpg);
  }
}

/* Imagen para dispositivos de alta resolución y el ancho mínimo de 468px */
@media (min-device-pixel-ratio: 2) and (min-width: 468px), (min-resolution: 192dpi) and (min-width: 468px), {
  .element {
    background-image: url(image-468@2x.jpg);
  }
}

/* Imagen para dispositivos de baja resolución y el ancho mínimo de 600px */
@media (min-width: 600px) {
  .element {
    background-image: url(image-600.jpg);
  }
}

/* Imagen para dispositivos de alta resolución y el ancho mínimo de 600px */
@media (min-device-pixel-ratio: 2) and (min-width: 468px), (min-resolution: 192dpi) and (min-width: 468px), {
  .element {
    background-image: url(image-600@2x.jpg);
  }
}
Advertencia

Te recomiendo ver el soporte actual ya que varios navegadores necesitan de prefijos -webkit-min-device-pixel-ratio, -moz-min-device-pixel-ratio, etc.

Lo sé, lo sé, mucho código para algo tan sencillo, eso sin contar los prefijos y el uso de Media Queries más complejos. Afortunadamente existe otra alternativa, la mala noticia es que solo pocos navegadores tienen soporte.

image-set:

Ver soporte

Es un método para servir la imagen correcta dependiendo la resolución del usuario, algo así como los Media Queries que acabas de ver, pero más fácil de lograr y leer.

.element {
  background-image: image-set(
    url(image.jpg) 1x,
    url(image@2x.jpg) 2x,
    url(image@3x.jpg) 3x
  );
}

Así de sencillo ya tienes una forma de servir tres tipos de imagen dependiendo la resolución. Recreemos el ejemplo anterior, pero ahora usando image-set.

.element {
  background-image: image-set(
    url(image.jpg) 1x,
    url(image@2x.jpg) 2x,
    url(image@3x.jpg) 3x
  );
}

@media (min-width: 468px) {
  .element {
    background-image: image-set(
      url(image-468.jpg) 1x,
      url(image-468@2x.jpg) 2x,
      url(image-468@3x.jpg) 3x
    );
  }
}

@media (min-width: 600px) {
  .element {
    background-image: image-set(
      url(image-600.jpg) 1x,
      url(image-600@2x.jpg) 2x,
      url(image-600@3x.jpg) 3x
    );
  }
}

Genial, ¿no crees? Acabamos de lograr lo mismo, con menos líneas de código, más fácil de mantener y entender.

Nota

Para los ansiosos como yo, hay varias formas de usar image-set actualmente, mi forma preferida es usando postcss y postcss-image-set-function, aunque si no usas postcss, puedes optar por un pollyfill.

Preload y Prefetch

Con preload y prefetch puedes informarle al navegador que recursos quieres que sean tomados y cuando se deberían cargar.

  • Preload se usa cuando necesitamos que la descarga sea inmediata (al mismo tiempo que se este leyendo el HTML).
  • Prefetch es todo lo contrario, se usa cuando la descarga no tiene tanta prioridad, se va a necesitar en algún punto, pero no es importante que se cargue de inmediato.

Uso

Solo necesitamos de la etiqueta <link> dentro de <head> y definir ciertos atributos.

<link rel="preload" href="image.jpg" as="image" media="(min-width: 460px)">
<link rel="prefetch" href="other-image.jpg" as="image" media="(min-width: 468px)">
  • rel ayuda a definir si usar preload o prefetch.
  • href define la ruta del archivo.
  • as el tipo de archivo.
  • media para definir en que tamaño de pantalla va a cargar la imagen.

Para que lo tengas un más claro, tu HTML debería verse de la siguiente manera:

<!DOCTYPE html>
<html>
  <head>
    <!-- Preload & Prefetch -->
    <link rel="preload" href="image.jpg" as="image" media="(min-width: 320px)">
    <link rel="prefetch" href="other-image.jpg" as="image" media="(min-width: 320px)">
  </head>
  <body>
    <header>
      <!-- Imagen 1 -->
      <img src="image.jpg" alt="preload image">
    </header>

    <section>...</section>

    <footer>
      <!-- Imagen 2 -->
      <img src="other-image.jpg" alt="prefetch image">
    </footer>
  </body>
</html>
Carga de archivos antes de usar preload
Carga de archivos después de usar preload
Tip

Es importante que no hagas uso de estos métodos para cargar todos los recursos de un sitio. Esto puede resultar en que satures la carga inicial de tu sitio web.

Puedes usar preload y prefetch para archivos CSS, javascript, fuentes, videos, audios, etc. Pero para eso te dejo un excelente artículo escrito por Yeison Daza que explica a fondo el tema.