Lazy-load con IntersectionObserver API y Vue 3

Aprende a utilizar la Intersection Observer API con la Composition API de Vue 3 para agregarle reactividad y permitir hacer lazy-load de colecciones y elementos


Hasta hace poco, saber cuando un elemento del DOM se encuentra visible dentro de viewport era una tarea que requería cálculos que afectaban al rendimiento (espiar el evento scroll, resize, etc). Ya no.

Ahora podemos utilizar la IntersectionObserver API, que además tiene un excelente soporte entre navegadores.

No te dejes engañar por el nombre: parece más complicado de lo que realmente es. Vamos a ver cómo usar esta nueva API del DOM con Vue 3.

Primeros pasos

Imagina esta situación: tienes una colección de datos (500 items para ser exactos) y quieres mostrarlos poco a poco, por ejemplo de 25 en 25. La clave aquí es que tendrías que averiguar cuándo se muestra el último de los primeros 25 elementos para luego pedir otros 25 y así sucesivamente hasta que todos se estén cargando.

Vamos a preparar parte de la lógica y estado del componente Vue.

<script setup>
    import { ref, onMounted, computed, nextTick } from 'vue';
    
    // RAW collection (all items)
    const collection = ref(Array.from(Array(500).keys()));
    
    // How much items to load in each step
    const stepLoad = 25;
    
    // How many items are loaded (starts with stepLoad value)
    const loaded = ref(stepLoad);
    
    // The collection lazy loaded
    const collectionLoaded = computed(() =>
      collection.value.slice(0, loaded.value)
    );
    
    // Template refs for each item
    const itemsRef = ref([]);
    
    // The las item loaded
    const lastItemRef = computed(() => itemsRef.value[loaded.value - 1]);
</script>
    
<template>
    <ul>
      <li
        ref="itemsRef"
        v-for="(item, index) in collectionLoaded"
        :key="index"
      >
        {{ item }}
      </li>
    </ul>
</template>

Vamos a repasar este snippet:

  • En collection encontrarás un Array con 500 elementos: números de 0 a 499.
  • stepLoad nos indica cuántos items queremos cargar con cada paso.
  • La constante loaded guarda cuántos items se han cargado por el momento. Se inicia con el valor de stepLoad.
  • Dentro de collectionLoaded guardamos una propiedad computada que nos devuelve el rango adecuado de items gracias al método slice de Array.
  • En itemsRef guardamos un Array de referencias reactivas a templates. Usaremos elementos de este Array para pedir que sean observados.
  • Por último, en lastItemRef tenemos una propiedad computada que nos devuelve el último template ref disponible.

Usar IntersectionObserver API con Vue 3 Composition API

Ahora que tenemos todo listo, es hora de usar la IntersectionObserver API para ir cargando más de los 25 items iniciales.

Primero, instanciemos la nueva API.

<script setup>
    ...
    
    // IntersectionObserver setup
    const observer = new IntersectionObserver((entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          console.log('Intersecting 👋');   
        }
      });
    });
</script>

Como ves, la instancia observer permite que actúes via callback (por ahora con un console.log) cuando alguno de los elementos del DOM que luego designemos (entries) haga intersección con el viewport de la página, es decir, que sea visible dentro del mismo.

Ahora llega la parte divertida, vamos a observar el último elemento DOM de la lista generada por collectionLoaded. Ten en cuenta que ese elemento siempre estará en lastItemRef.

<script setup>
    ...
    
    const startObserving = async (target = lastItemRef) => {
      await nextTick();
      observer.observe(target.value);
    };
    
    onMounted(() => {
      startObserving();
    });
</script>

Vamos poco a poco:

  • Hemos creado un método local startObserving que le pide a IntersectionObserver que observe la intersección de un elemento del DOM: observer.observe(target.value).
  • Date cuenta que usamos nextTick y una función async para asegurarnos de que efectivamente el elemento nuevo está disponible en el DOM.
  • Usamos el ciclo de vida mounted del componente con onMounted para lanzar el método recién creado.

Lazy load de más elementos

Todo esto está genial pero, en realidad, lo único que estamos haciendo es mostrar 'Intersecting 👋' en la consola cuando el elemento nº 25 aparece en el viewport. Nosotros, lo que queremos, es cargar otros 25.

Para ello volvamos al callback cuando creamos la instancia de IntersectionObserver para añadir la lógica necesaria.

<script setup>
    ...
    
    // IntersectionObserver setup
    const observer = new IntersectionObserver((entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
            ...
            entry.target.style.backgroundColor = 'red';
            loaded.value += stepLoad;
            startObserving();
        }
      });
    });
</script>

Repasemos punto por punto:

  • Primero cambiamos el fondo del elemento del DOM (accesible dentro de entry.target) para poder visualizar más fácilmente la intersección del elemento que pone todo el mecanismo en marcha.
  • Añadimos 25 al valor de loaded. Esto dispara la reactividad de las propiedades collectionLoaded y lastItemRef ( ya que loadaed forma parte de ellas), añadiendo más items a mostrar y recalculando la última referencia a template.
  • Esta última referencia reactiva es la que enviamos acto seguido (es el valor de parámetro por defecto) con el método startObserving.

Parar el observador

Ahora que cada vez que cargas 25 items y haces scroll hasta el final, se cargan otros 25, solo queda comprobar cuándo no hay más items que cargar para desconectar el observador.

Se puede hacer de muchas formas, pero yo lo he dividido en dos partes.

Primero, creamos un método que lance la acción disconnect a observer.

<script setup>
    ...
    const stopObserving = () => observer.disconnect();
</script>

Y luego hago la comprobación adecuada (si la cantidad de items cargada es igual a la cantidad de items total) en el callback que hemos usado hasta ahora.

<script setup>
    ...
    
    // IntersectionObserver setup
    const observer = new IntersectionObserver((entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
            ...
    
            if (loaded.value === collection.value.length) return stopObserving();
        }
      });
    });
</script>

Conclusiones

Como ves, es muy sencillo usar la nueva API IntersectionObserver con la Composition API de Vue 3 y hacerlo reactivo. Este es el código resultante.

Esta técnica, a su vez, tiene muchos usos prácticos. El más obvio es (como hemos hecho nosotros) hacer lazy-load de elementos para mejorar el rendimiento y usabilidad, pero también se puede utilizar para lazy-load de imágenes (haciendo un swap del atributo src cuando entran al viewport, por ejemplo).

Demo

Juan Andrés Núñez
Juan Andrés Núñez
Ingeniero Frontend. Especialista en Vue.js. Docente profesional. Estoico.