"El buen diseño es obvio. El gran diseño es transparente"
- Joe Sparano

Intersection Observer

7 minutos de lectura
Fecha: 28/3/2018

Intersection Observer es una API incluida en el estandar de HTML5 que nos va a resultar muy útil principalmente para realizar el lazy-load de una manera más sencilla y mantenible.

La carga perezosa, más conocida como lazy-load, sirve para cargar ciertos recursos únicamente cuando realmente se vayan a mostrar en la pantalla, su uso principal es con las imágenes.

El problema

Las imágenes son recursos bastante pesados, sobretodo cuando se trata de imágenes de gran tamaño, y además para estos casos la estrategia de usar sprites no es válida, de manera que cada una de ellas es una petición.

El tener que cargar de inicio muchas imágenes implica tener que hacer muchas peticiones, y descargar recursos pesados lo que provoca que al final la página tarde una eternidad en cargar.

En páginas como e-commerce donde el objetivo es que el usuario permanezca (si se va no compra) y donde lo habitual es tener un número ingente de imágenes, la velocidad es imprescindible.

Las soluciones

Se puede optar por usar formatos más modernos de compresión para obtener imágenes más livianas, por ejemplo ya hablé en otras entradas de los formatos WebP y HEIF.

Se puede optar por usar la etiqueta picture (también puedes pulsar para ver la entrada), y con ello pedir distintos tamaños y formatos de imágen en función del dispositivo.

Y se puede usar la caché para aumentar la velocidad para servirlas.

Sin embargo la mejor forma y la más efectiva de aumentar la velocidad de carga de tu página web cuando tienes un caso así es usar lazy-load y para eso tienes la API Intersection Observer.

Por supuesto, la estrategia que una empresa seria debe seguir no es escoger entre una de estas opciones, sino usarlas todas.

¿Como funciona el lazy-load?

Como antes se mencionó, lazy-load se basa en cargar las cosas solo cuando las necesitas, solo cargas las imágenes cuando estas son visibles en pantalla.

Y tiene toda la lógica, ¿si tienes una página con 200 fotos para que quieres cargar de inicio todas?, al fin y al cabo si el usuario se queda mirando las primeras y luego cierra la ventana te podías haber ahorrado todas las demás, la página habría cargado más rápido y no habrías obligado al usuario a descargar todas esas imágenes, que en el caso de un teléfono con una tarifa de datos sí que le perjudica.

Lazy-load propone que la imágen se pida y se cargue cuando el usuario haga scroll y llegue hasta la zona donde se encuentra.

Por supuesto, tiene la desventaja de que si el usuario hace scroll rápido durante unos instantes, (el tiempo que tardemos en recibir y renderizar la imagen) no va a verla, y tendremos que poner un preload para que sepa lo que está sucediendo.

Pero este pequeño inconveniente no es nada comparado con lo que ganamos al usarlo.

¿Como aplicar lazy-load a las imágenes?

La base para aplicarlo es la misma estemos usando listeners para escuchar donde esta el scroll, o estemos usando la nueva API Intersection Observer.

Lo esencial es que si nosotros a especificamos el valor al atributo src de una imagen esta de forma automática va a intentar recogerla, como mínimo hará la petición, y si además la devuelve la va a pintar.

Nosotros cuando hacemos lazy-load no queremos que esto se haga automáticamente, por eso el atributo src debe quedar vacío.

Por supuesto en algún lado tenemos que dejar la url donde esta la imagen, por convención usamos el atributo data-src, de manera que quedaría algo así…

<img src="" data-src="nuestra-imagen.jpg">

Hasta aquí todo es común, ahora es cuando tenemos que ver si las imágenes están en nuestro viewport, es decir, las vemos en la pantalla. Y si las vemos pues las cargamos.

Usando la API Intersection Observer

Antiguamente esta tarea se hacía mediante listeners sobre el scroll que fueran dando información sobre donde estábamos, para después comparar si ya habíamos llegado a un punto donde se encontraba alguna imagen, y pedirla.

Esto se hacía así y era lo que había, pero era una tarea muy costosa a todos los niveles, tanto para el programador como para el equipo al que a veces saturábamos de tanto escuchar eventos.

Con Intersection Observer podemos hacer esta tarea de forma más sencilla para todos.

En primer lugar hay que tener en cuenta que esta API es relativamente nueva así que no todos los navegadores la soportan, puedes ver aquí cuales son, por resumir edge, firefox y chrome la soportan, pero Explorer y Safari no.

Para que no tengamos problemas antes de comenzar a usarla podemos preguntarle al navegador si tiene esa capacidad, lo podemos hacer manualmente con estas lineas.

if (!('IntersectionObserver' in window)) {
  console.log("No está disponible");
} else {  
  console.log("Tenemos Intersection Observer!");
}

En caso de no tenerlo aplicamos la manera antigua, que como no es el propósito del post no voy a explicar.

Suponiendo que esta disponible vamos a crear un el objeto mediante el constructor.

var options = {
  root: null,
  rootMargin: '0px',
  threshold: 1.0
}

var observer = new IntersectionObserver(imgLazyLoad, options);

Como vemos las opciones que le pasamos son 3:

  1. root – Es el selector del elemento sobre el que queremos usar como viewport, o dicho de otra forma, el elemento de referencia que actua de observador, en caso de no definirlo o ponerlo a null será el propio documento (que es lo más habitual).
  2. rootMargin – Un parámetro muy útil donde definimos el margen con el que observarlo, 0px es tan pronto entre, 10px serían que se dispara 10px después de entrar y -10px lo dispararía cuando este a 10px de entrar.
  3. threshold – es el umbral, los valores van de 0 a 1, y sirve para disparar la función de callback cuando el umbral se alcance, el valor por defecto es 0, pero por ejemplo, si pusieramos 0.5 significaría que el callback se dispara cuando la mitad del elemento este dentro del viewport, (podemos usar un array para escuchar observar distintos puntos)

Ahora tenemos que decir que elementos vamos a vigilar con el Intersection Observer, y como de lo que estamos hablando es de las imágenes por ser el caso más frecuente voy a hacerlo sobre ellas, pero solo las que tengan una clase en concreto.

Por ejemplo todas las imagenes que tengan la clase objetivo.

var images = document.querySelectorAll(‘.objetivo’);

images.forEach(image => {
  observer.observe(image);
});

Cuando se produce la intersección se llamará a la función de callback que en este caso le he llamado imgLazyLoad, así que tenemos que declararla y dentro escribimos lo que queremos que haga.

Como antes explicamos tenemos que coger el valor de nuestro atributo personalizado data-src y aplicárselo al src, de esta manera se carga la imagen.

function imgLazyLoad(observer) {
  $element = $(observer[0].target)           
  if($element.attr("src").length === 0) $element.attr("src", $element.data("src"));
}

Estoy usando Jquery por comodidad pero puedes adaptar tu código si no quieres usarlo porque utilizas angular, react, etc y no te hace falta, o sencillamente porque prefieres el vanillaJS.

Lo único que hago a mayores es comprobar antes si ya tiene algo en el atributo src, porque la intersección se va a disparar tantas veces como pasemos con ella por encima pero yo solo quiero que esta lógica se aplique la primera vez.

Con este código ahora a medida que hagamos scroll nuestra página ira cargando las fotos que vayan entrando en el viewport, se puede comprobar tanto visualmente, como inspeccionando las peticiones de red con las herramientas de desarrollador del navegador que usemos.

Otros usos

Aunque el lazy load de imágenes es el primer caso de uso que se me viene a la cabeza, no es el único, puedes usarlo para otras cosas, por ejemplo:

  1. Si lo usas para hacer lazy load de contenidos podrías emplear la técnica del scroll infinito para que a medida que vayas bajando se carguen más contenidos, obviamente no es lo mismo que las cambiar el valor de un atributo como en el caso anterior, seguramente requiera de hacer peticiones a una api, pero tampoco es mucho más complejo.
  2. Puede servirte para disparar animaciones, el problema de usar animaciones css/js es que salvo que uses librerías de terceros, las animaciones se disparan sobre el elemento cuando el código es procesado, de manera que salvo que te coincida de tener el elemento visible en pantalla en ese momento te las vas a perder, con Intersection Observer puedes usar una manera nativa de dispararlas correctamente.