Mejorando la carga de imágenes y SVGs

El primer paso y el más importante para mejorar la velocidad de carga de un sitio son las imágenes. Si todavía no sabes bien como medir la velocidad de tu sitio, el siguiente artículo puede ayudarte:

Midiendo la velocidad de tu sitio

Si ya trabajaste optimizando las imágenes, quizás te interese leer sobre otros cambios que pueden mejorar la velocidad:

Mejorando la carga de imágenes y SVGs

Mejorando la carga del CSS

Imágenes

Formato

Basándonos en pruebas que fuimos haciendo en nuestras tiendas de Tienda Nube, descubrimos que si bien el formato PNG se ve con una buena calidad puede generar un peso innecesario, el cual se puede reducir hasta 10 veces cuando cambiamos al formato JPG. Claro está que si necesitás que la imagen tenga el canal alpha (es decir que use transparencia), JPG entonces no va a poder ser una opción, pero es importante tenerlo en el radar.

Producto de la tienda Unibow, una Tienda Nube donde la imagen pasó de pesar 2,5mb a 181 kb.

En el ejemplo de Unibow, el tiempo de carga de las imágenes luego de modificar el formato y comprimir usando herramientas como TinyPNG ¡Bajó de los casi 8.67 segundos a 2.07, son 6 segundos menos!

Arriba el antes y debajo el después del cambio.


LazyLoad

¿Para qué cargar todas las imágenes si el usuario todavía no necesita verlas? Pueden lograr esto usando la técnica de “LazyLoad” que básicamente lo que hace es cargar todas las imágenes que se encuentran “above the fold” (dentro del marco inicial en la pantalla cuando se carga el sitio) o estén cerca estar visibles en la pantalla, y luego las va cargando de forma progresiva a medida que el usuario va haciendo scroll o abre una pantalla o popup que tiene estas imágenes que no fueron cargadas.

De esta forma se ahorra una demora enorme hasta que el documento termina de cargar todas las imágenes, ¿Para qué llamar a imágenes que están en el footer o en un popup apenas carga la página?.

Hay decenas de plugins de JS para usar LazyLoad, en Tienda Nube usamos lazysizes que es bastante completo y fácil de implementar. Algunos puntos sobre lazysizes:

  • No depende de Jquery, importante para cargar lo mínimo de código bloqueante (Javascript o CSS).
  • Es fácil de usar, simplemente agregando la class “lazyload” a la img, luego al atributo src se le pasa la url de un placeholder (o puede ser la misma imagen en baja calidad) y finalmente usando el atributo data-src (o data-srcset si estás necesitan srcset) con la url de la imagen final en buena calidad. Una vez que cargue la imagen, la class “lazyload” va a pasar a ser “lazyloaded”, esto podés usarlo para hacer cambios CSS una vez que se terminó la carga.
  • Si vas a usar srcset es importante considerar las extensiones respimg(para que los browsers vintage como IE 9 y 11, lo odio con todo mi corazón) y bgset en caso que necesiten usar una imagen como background-image sumado a srcset.
  • Otro punto interesante es que el plugin no va a cargar las imágenes que tengan o estén englobadas por un elemento con un "display:none;".
  • Si necesitás exprimir más el jugo del plugin podés darle una ojeada a las distintas extensiones que tiene

Placeholders

Tenés LazyLoad, ¡Genial! Pero ¿Qué pasa mientras no se muestra la imagen final?. Pregunta CLA-VE, no se si quedó claro =P

Se pueden hacer varias cosas hasta tener la imágen final, mostrar un icono girando (spinner), mostrar un cuadrado o rectángulo gris del tamaño de la imagen, mostrar un SVG representando lo que va a ser la imagen (como hacen muchas apps por ejemplo Facebook), usar la misma imagen en baja calidad. Siempre mostrar algo, ayuda mucho a bajar la ansiedad del usuario (a diferencia de una pantalla en blanco), pudiendo continuar navegando el contenido y mejorando la percepción de velocidad (el sitio parece que carga más rápido pero en realidad no es así. Esto es tan importante como la velocidad real ya que es lo que a fin de cuentas va a percibir el usuario).

Ejemplo de un placeholder en Facebook, del artículo “Improved Percieved Performance With Skeleton Screens”

En nuestras tiendas lo que hicimos fue aplicar la técnica de LQIP (Low Quality Image Placeholders) cargando la imagen en un tamaño muy chico con un efecto blur hasta que lazy load muestre la imagen final. A su vez para los sliders mezclamos esto con un placeholder tipo “wireframe” (usamos bxslider y simplemente ocultamos el placeholder en el evento onSliderLoad) :

Para aplicar el LQIP, hay que aplicar al src del tag img la imagen en baja calidad y peso (en Tienda Nube tenemos varias versiones de la misma imagen, las más chica con 50px de ancho, los tamaños que usamos son: tiny de 50px de ancho, thumb de 100px, small de 240px, medium de 320px, large de 480px, huge de 640px y original de 1024px) con un CSS que le da width al 100% de su contenedor y luego cuando la imagen es “lazyloadeada” se reemplaza por la imagen en buena calidad. En el medio se pueden usar transiciones con CSS, nosotros aplicamos un efecto de blur animado.

El código queda más o menos así:

<img alt=”{{ product.featured_image.alt }}” data-sizes=”auto” src=”{{ product.featured_image | product_image_url(‘tiny’)}}” data-src=”{{ product.featured_image | product_image_url }}” class=”lazyload item-image img-absolute blur-up” />

Donde se puede ver que estamos usando la class “lazyload” para usar lazysizes, la class blur-up para la transición de “imagen blureada” a “imagen final” y los atributos src para la imagen placeholder en baja calidad (tiny) y data-src para la imagen final (más adelante vamos a usar el mismo ejemplo con srcset).

El CSS para la transición es el siguiente:

.blur-up {
  -webkit-filter: blur(6px);
  filter: blur(6px);
  transition: filter .5s, -webkit-filter .5s;
}
.blur-up.lazyloaded {
  -webkit-filter: blur(0);
  filter: blur(0);
}

Usando la class “lazyloaded” puede ayudar para incluso mostrar un icono y luego ocultarlo una vez que se cargó la imagen.

Aún usando el código anterior el resultado no sigue siendo el mejor, vas a tener saltos visuales entre qué mostramos la imagen chica (o puede ser un icono placeholder) y la imagen final. Para evitar esto necesitás saber el espacio que va a ocupar la imagen antes de esta sea cargada y para esto necesitás conocer las dimensiones de alto y ancho desde el backend.
Con esta información vas a tener que hacer lo siguiente:

<div style="padding-bottom: {{ product.featured_image.dimensions['height'] / product.featured_image.dimensions['width'] * 100}}%;">
    <a href="{{ product_url_with_selected_variant }}" title="{{ product.name }}">
        <img alt="{{ product.featured_image.alt }}" data-sizes="auto" src="{{ product.featured_image | product_image_url('tiny')}}" data-src="{{ product.featured_image | product_image_url }}" class="lazyload item-image img-absolute blur-up" />
    </a>
</div>

Una vez cargado en el navegador se va a ver así:


Vas a notar dos cosas:

  • Hay un estilo inline sobre el contenedor de la imagen, que básicamente es la cuenta: alto/ancho*100. Está cuenta les va a dar un porcentaje que van a usar como un padding-bottom , el cual va a “empujar” el espacio que ocupará la imagen final con la relación de aspecto correcta (muy útil también en grillas tipo Pinterest). La cuenta va a dar algo como padding-bottom: 133.68146214099%; .
  • Por otro lado necesitás que la imagen se ubique bien dentro del contenedor sin que sea empujada por ese padding. Para eso usen la class img-absolute con el siguiente CSS:
.img-absolute {
  position: absolute;
  left: 0;
  width: 100%;
  height: auto;
  vertical-align: middle;
  text-indent: -9999px;
  z-index: 1;
}

SRCSET

Otro cambio clave es la incorporación de srcset, ¿Qué es esto? Básicamente imágenes responsive a través de un atributo de HTML.

Agregando el atributo srcset además del src dentro del tag img, podés pasarle varias urls de la misma imagen en distintos tamaños y el navegador al cargar se va a encargar de cargar la imagen mas adecuada en base a por ejemplo el ancho de la pantalla.


En las tiendas usamos la implementación basada en la unidad “w”, a la cual le pasamos el ancho de cada imagen que tenemos. Hay varias formas de usar srcset, te invito a leer más sobre las distintas posibilidad es que ofrece este atributo.

Te dejo un ejemplo de como se ve el código juntando lazyload + srcset:

<div style="padding-bottom: {{ product.featured_image.dimensions['height'] / product.featured_image.dimensions['width'] * 100}}%;">
    <a href="{{ product_url_with_selected_variant }}" title="{{ product.name }}">
        <img alt="{{ product.featured_image.alt }}" data-sizes="auto" src="{{ product.featured_image | product_image_url('tiny')}}" data-src="{{ product.featured_image | product_image_url('small')}} 240w, {{ product.featured_image | product_image_url('medium')}} 320w" class="lazyload item-image img-absolute blur-up" />
    </a>
</div>

SVGs

El SVG también es un formato gráfico como la imagen y mejora considerablemente la velocidad de carga ya que es un vector que no demora tanto en “ser pintado” por el navegador como una imagen clásica donde se tiene que pintar/cargar pixel por pixel.

En nuestro caso usamos SVGs para cargar todos los iconos del sitio, de hecho antes usábamos Font Awesome pero lo removimos y pasamos todo a SVGs para no ahorrar el llamado a un CSS tipográfico con cientos de íconos cuando solo necesitábamos algunos.

Para usar los SVGs lo que tenés que hacer es:

  • Crear el SVG en un software como Illustrator o el que te sientas cómodo, o descargá un SVG hecho.
  • Desde illustrator vas a la opción de “guardar como” y elegís el formato SVG Tiny 1.1.


  • Luego elegís la opción de código SVG.


  • Copiás ese código y lo pegás en el sitio SVGOMG que te va a ayudar a comprimirlo aún más. Hacé click en el icono de “copiar”.

Ya tenés el código SVG listo para ser pegado donde quieran, es importante que este inline en la maqueta para evitar un pedido del navegador innecesario. En Tienda Nube generamos un archivo para cada SVG y luego lo importamos con Twig usando un include que básicamente hace lo mismo que si lo pegaran inline en el HTML.

El archivo creado es un .tpl que usa el atributo {{ svg_custom_class }} .

<svg class="{{ svg_custom_class }}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 38 32.5"><path d="M18.5,28.5a4,4,0,1,1-4-4A4,4,0,0,1,18.5,28.5Zm11-4a4,4,0,1,0,4,4A4,4,0,0,0,29.5,24.5Zm5.44-3.44,3-13A2.5,2.5,0,0,0,35.5,5H16a2.5,2.5,0,0,0,0,5H32.36l-1.85,8h-17L9.94,2A2.49,2.49,0,0,0,7.5,0h-5a2.5,2.5,0,0,0,0,5h3L9.06,21a2.49,2.49,0,0,0,2.44,2h21A2.51,2.51,0,0,0,34.94,21.06Z"/></svg>

Este atributo lo usamos para pasarle la class que necesitemos al incluirlo:

{% include "snipplets/svg/cart.tpl" with {svg_custom_class: "icon-inline icon-2x"} %}

A su vez usamos un CSS que define distintos tamaños para los SVGs, muy similar a lo que usa Font Awesome.

.icon-inline {
  display: inline-block;
  font-size: inherit;
  height: 1em;
  overflow: visible;
  vertical-align: -.125em;
}


.icon-xs {
  font-size: .75em;
}
.icon-sm {
  font-size: .875em; 
}
.icon-lg {
  font-size: 1.33333em;
  line-height: .75em;
  vertical-align: -.0667em; 
}
.icon-2x {
  font-size: 2em;  
}
.icon-3x {
  font-size: 3em; 
}
.icon-4x {
  font-size: 4em;  
}
.icon-5x {
  font-size: 5em;  
}
.icon-6x {
  font-size: 6em;  
}
.icon-7x {
  font-size: 7em; 
}
.icon-8x {
  font-size: 8em;  
}
.icon-9x {
  font-size: 9em;  
}


.icon-inline.icon-lg{
  vertical-align: -.225em
}
.icon-inline.icon-w {
  text-align: center;
  width: 1.25em
}
.icon-inline.icon-w-1{
  width:.0625em
}
.icon-inline.icon-w-2{
  width:.125em
}
.icon-inline.icon-w-3{
  width:.1875em
}
.icon-inline.icon-w-4{
  width:.25em
}
.icon-inline.icon-w-5{
  width:.3125em
}
.icon-inline.icon-w-6{
  width:.375em
}
.icon-inline.icon-w-7{
  width:.4375em
}
.icon-inline.icon-w-8{
  width:.5em
}
.icon-inline.icon-w-9{
  width:.5625em
}
.icon-inline.icon-w-10{
  width:.625em
}
.icon-inline.icon-w-11{
  width:.6875em
}
.icon-inline.icon-w-12{
  width:.75em
}
.icon-inline.icon-w-13{
  width:.8125em
}
.icon-inline.icon-w-14{
  width:.875em
}
.icon-inline.icon-w-15{
  width:.9375em
}
.icon-inline.icon-w-16{
  width:1em
}
.icon-inline.icon-w-17{
  width:1.0625em
}
.icon-inline.icon-w-18{
  width:1.125em
}
.icon-inline.icon-w-19{
  width:1.1875em
}
.icon-inline.icon-w-20{
  width:1.25em
}
.icon-spin{
  -webkit-animation:icon-spin .5s infinite linear;
  animation:icon-spin .5s infinite linear
}
@-webkit-keyframes icon-spin {
  0% {
    -webkit-transform: rotate(0);
    transform: rotate(0)
  }
  100% {
    -webkit-transform: rotate(360deg);
    transform: rotate(360deg)
  }
}


@keyframes icon-spin {
  0% {
    -webkit-transform: rotate(0);
    transform: rotate(0)
  }
  100% {
    -webkit-transform: rotate(360deg);
    transform: rotate(360deg)
  }
}

¿Qué sigue?

Si ya pudiste optimizar todo lo que tiene que ver con imágenes te recomiendo seguir leyendo los cambios que aplicamos sobre los otros dos frentes para mejorar la velocidad:

Mejorando la carga del Javascript

Mejorando la carga del CSS