, ,

Lazy load images con IntersectionObserver API

Hace años que existe la carga de imágenes de forma perezosa, más conocida como “Lazy load images”. Este método utiliza un temporizador periódico que observa el evento de desplazamiento con getBoundingClientRect(). Esta técnica supone una penalización importante ya que fuerza el repintado de toda la página. IntersectionObserver ha nacido para resolver este problema, permitiendo una mejora sustancial de rendimiento y optimización en nuestro sitio web.

¿Qué es IntersectionObserver?

IntersectionObserver provee a los navegadores una forma asíncrona y nativa de observar los cambios visuales cuando un elemento cualquiera (iframe, div, imagen…) aparece en nuestra pantalla. Algunas de las posibles aplicaciones de IntersectionObserver son las siguientes:

  • Implementación de scroll infinito.
  • Carga perezosa de imágenes (lazy load images).
  • Reporte de visibilidad de anuncios para calcular los ingresos publicitarios.
  • Decidir si se realizarán tareas o procesos de animación.

En este artículo vamos a realizar un pequeño ejemplo para la carga perezosa de imágenes (lazy load images). Hoy en día, cualquier sitio web dispone de imágenes, y aunque ya estemos aplicando técnicas como la compresión de imágenes o uso de srcset, si nuestro sitio web dispone de muchas imágenes posiblemente el cliente se encuentre ocupado descargando imágenes (no visibles, es decir, fuera del viewport) en detrimento de recursos más críticos como puedan ser CSS, JS o el propio HTML.

Implementar IntersectionObserver

Constructor

var io = function(entries, config) { 
  entries.forEach(entry => {
    // Propiedades que son observadas en un elemento
    // target element:
    //   entry.boundingClientRect
    //   entry.intersectionRatio
    //   entry.intersectionRect
    //   entry.isIntersecting
    //   entry.rootBounds
    //   entry.target
    //   entry.time
  });
};

El primer parámetro es una función callback que será ejecutada cada vez que un elemento observado por IntersectionObserver cumpla con los siguientes requisitos:

  • Ingrese parcialmente en el viewport (threshold)
  • Salga completamente del viewport

El segundo parámetro es el de configuración. Para el que recomiendo crear una variable y utilizarla posteriormente en la creación de IntersectionObserver:

var config = {
  // Elemento referencia para IntersectionObserver
  // Por defecto: la referencia será Document (viewport)
  root: null,
  // Al igual que la propiedad margin de CSS puede tener 4 parámetros (+ o -).
  // Si has definido root, puedes utilizar porcentajes(%)
  rootMargin: "0px",
  // Define en qué puntos se activará la llamada a la función callback. Estos puntos
  // tienen que estar comprendidos por valores en 0 y 1. No se recomiendo utilizar 1.
  threshold: [0],
};

Métodos

  • disconnect(): detiene la actividad de IntersectionObserver.
  • observe(): comienza la actividad sobre un elemento.
  • takeRecords(): devuelve un Array de Entries, y termina la actividad.
  • unobserve(): para la actividad de un elemento en concreto.

Ejemplo práctico

Lo mejor es realizar un ejemplo sencillo en el que, como os he comentado anteriormente, vamos a usar IntersectionObserver para realizar la carga perezosa de imágenes (lazy load images). Pero os recuerdo que podéis “observar” cualquier elemento alcanzable del DOM.

HTML

Las imágenes tendrán el atributo data-src en detrimento de src. Además todas las imágenes tendrán asignado un class ‘lazy-image’ .

CSS

Lo único destacable es una función de animación fadeIn para dar el efecto de carga a las imágenes observadas.

 Javascript
// ZONA A
var images = document.querySelectorAll('.lazy-image');
var num_images = images.length;

var config = {
  root: null,
  rootMargin: "0px",
  threshold: 0.01,
};

// ZONA B
var io;

'IntersectionObserver' in window ?
    (
        io = new IntersectionObserver(onIntersection,config),
        images.forEach( image => { io.observe(image); })    
    )
    : (
        loadAllNow(images)
    );

// ZONA C
function applyImage(image,src){    
    image.classList.add('lazy-image--handled');
    image.src = src;
    image.classList.add('fade-in');
}

function preloadImage(image){
    const src = image.dataset.src;
    if (!src) return;

    return applyImage(image,src);
}

function loadAllNow(images){
    Array.from(images).forEach(
        image => preloadImage(image)
    );
}

function onIntersection(entries){
    entries.forEach( entry => {
        if (!entry.isIntersecting){ return; }

        num_images--;
        io.unobserve(entry.target);
        preloadImage(entry.target);
    });
    if (num_images > 0){ return; }

    io.disconnect();
}
ZONA A

Recuperamos todas las imágenes que van a ser tratadas en nuestro HTML, esto es, aquellas que tengan el class ‘lazy-image’ y guardando también la cantidad de imágenes encontradas. Por otra parte, crearemos una variable con la configuración correspondiente para root, rootMargin threshold.

ZONA B

Comprobamos que el navegador soporta IntersectionObserver ya que si no es así, debemos cargar las imágenes con la función loadAllNow. Si es soportado, crearemos un objeto io y observaremos cada una de las imágenes que hemos obtenido en la ZONA A. Especialmente relevante, la función onIntersection, desde la que trataremos todas y cada una de las imágenes en el momento que sean visibles.

ZONA C

Aquí se encuentran las funciones que tratan las imágenes, añadiendo el class ‘lazy-image–handled’ para aquellas imágenes que han sido tratadas y añadiendo el atributo src con la ruta de la imagen, cuya información se encuentra en data-src. En el momento que una imagen ha sido tratada, esta deja de ser observada con la función unobserve. Finalmente, una vez que todas las imágenes hayan sido tratadas , desconectamos io con la función disconnect.

Compatibilidad con IntersectionObserver

[ciu_embed feature="intersectionobserver" periods="-1,current,+1,+2"]

A día de hoy existe una gran compatibilidad con los principales navegadores en sus versiones estables. Sin embargo, es posible que el cliente no disponga de un navegador compatible con IntersectionObserverEn este caso, lamentablemente, no se cargarán las imágenes objetivo que tengan el class ‘lazy-image’. Por este motivo, debemos preguntar al navegador por la existencia de la API antes de realizar ninguna acción. Si no existiera, procederíamos a cargar las imágenes de forma normal sin contar con la API.

var io;

'IntersectionObserver' in window ?
    (
        io = new IntersectionObserver(onIntersection,config),
        images.forEach( image => { io.observe(image); })    
    )
    :(loadAllNow(images));

Polyfill (Buscando compatibilidad)

Si deseas mantener la carga perezosa para los navegadores que no soportan IntersectionObserver, puedes hacer uso del IntersectionObserver polyfill de W3C.

Artículos de referencia

https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver

https://developers.google.com/web/updates/2016/04/intersectionobserver

https://github.com/w3c/IntersectionObserver/blob/gh-pages/explainer.md

https://deanhume.com/Home/BlogPost/lazy-loading-images-using-intersection-observer/10163