Skip to content
En esta página

Lazy loading en Vue

El término lazy loading se podría traducir como carga perezosa o carga diferida. La idea es que el frontend cargue los archivos que necesita renderizar (imágenes, videos, componentes) recién en el momento en que precisa mostrarlos y no antes. Esto hace que la carga inicial de la aplicación sea más rápida.

En el caso de las imágenes o videos, que suelen ser archivos pesados, la diferencia entre cargarlos en forma normal y en forma lazy es muy notoria.

Lazy loading de imágenes

Por ejemplo, ésta es la carga de una imagen pesada sin lazy loading:

cheese cake

Y ésta es la carga de esa misma imagen con lazy loading:

cheese cake

Como pueden ver, al cargar imágenes grandes sin lazy loading éstas se van cargando gradualmente y se produce un efecto "cortina" que es bastante molesto. Esto es especialmente notorio cuando hay varias imágenes cargándose al mismo tiempo en una misma vista, como suele ocurrir en la vista principal de un e-commerce.

El problema al tener varias imágenes juntas es que el browser automáticamente descarga todas las imágenes que encuentra dentro de los elementos img en el archivo HTML, incluso las imágenes que el usuario ni siquiera puede ver porque están más abajo en la página y que recién verá cuando haga un scroll para bajar hasta el final de la página:

lazy loading

Entonces, lo primero que el usuario ve son las primeras imágenes de la página descargándose en forma de "cortina" como en el ejemplo de acá arriba. Y hasta que no estén todas las imágenes descargadas (incluso las que aún no vió) las primeras imágenes (que sí está viendo) no terminan de cargarse.

Lo que hace el lazy loading para solucionar este problema es descargar únicamente las imágenes que el usuario puede ver y luego ir cargando las demás imágenes a medida que el usuario hace scroll hacia abajo para ver el resto de la página. Es decir, a medida que las imágenes entran en el viewport (o sea, la parte de la página que es visible para el usuario en un determinado momento).

De esta forma, las primeras imágenes no tienen que esperar a que todas las imágenes estén cargadas para terminar de cargarse y se muestran mucho más rápido.

Intersection Observer

Hasta hace unos pocos años implementar el lazy loading de imágenes era bastante complejo.

Primero había que usar el atributo de datos modificables data-* para reemplazar el src (y también el alt) y así evitar que la imagen se cargue automáticamente apenas el browser lee el atributo img:

html
<img
  data-src="https://dav-leda.github.io/images-bakery/cheese-cake.jpg" 
  data-alt="cheese cake"
  style="opacity: 0; transition: opacity 1s ease-in-out;"
>

Y luego usar JavaScript para aplicar un intersection observer sobre todas las imágenes y así saber cuándo cada imagen se encuentra dentro del viewport.

Y cuando la imagen entra en el viewport asignarle al atributo src del elemento img el contenido del atributo data-src (que en JavaScript pasa a ser dataset.src). Y luego cambiar la opacity de 0 a 1 en el momento de la carga para crear un efecto fade in:

js
// Crear un array con todos los elementos img de la página
// usando querySelectorAll:
const images = Array.from(document.querySelectorAll('img'));
        
// IntersectionObserver toma 2 params: un callback y el margen de intersección
const observer = new IntersectionObserver( handleIntersect, 
  // El margen superior debe ser de al menos 100px para activarlo cuando el scroll es hacia arriba:
  { rootMargin: "100px 0px 0px 0px"} 
)
  
function handleIntersect(images) {
  // Dentro del callback iterar sobre el array de imágenes
  // e invocar un nuevo callback para cada una
  // pasándole por parámetro el atributo target:
  images.forEach(image => image.isIntersecting && loadImage(image.target))
}

function loadImage(image) {
  // Al cargar la imagen pasar la opacity de 0 a 1
  image.onload = () => image.style.opacity = 1
  // Una vez cargada la imagen pasar la url de data-src a src
  image.src = img.dataset.src
  image.alt = img.dataset.alt
  // Desconectar el observer para evitar memory leaks
  observer.disconnect()
}

Como ven, implementar un intersection observer es bastante enroscado.

Por suerte, a partir del 2020 los browsers (primero Chrome y luego los demás, como suele ocurrir) comenzaron a incorporar el lazy loading en HTML nativo, haciendo todo esto mucho más fácil.

Lazy loading nativo en HTML

Actualmente los browsers permiten usar en forma nativa el atributo loading asignándole la propiedad "lazy", y luego usar el atributo onload para crear el efecto fade in pasando la opacity de 0 a 1 en el momento en que la imagen se carga:

html
<img
  loading="lazy"
  style="opacity: 0; transition: opacity 1s ease-in-out;"
  onload="this.style.opacity = 1"
  src="https://dav-leda.github.io/images-bakery/cheese-cake.jpg" 
  alt="cheese cake"
  width="300"
>

Lazy loading en Vue

En Vue esto puede simplificarse aún más usando la directiva v-on:load, abreviada como @load:

html
<template>
  <img
    loading="lazy"
    class="product-image"
    @load="$event.target.style.opacity = 1"
    :src="product.image"
    :alt="product.title"
  />
</template>

<script>
export default {

  props: {
    product: {
      image: String,
      title: String
    }
  }
}
</stript>

<style scoped>

.product-image {
  height: 16rem;
  width: 14rem;
  object-fit: cover;
  opacity: 0;
  transition: opacity 1s ease;
}

</style>

Para crear el efecto fade in al cargar la imagen podemos usar la propiedad transition en la clase de CSS (.product-image) para que el cambio en la propiedad opacity dure 1 segundo.

Luego, cuando carga la imagen (@load) opacity pasa a ser 1 y la imagen se muestra gradualmente 🥳️

Lazy loading de componentes

Otro uso que tiene el lazy loading en Vue es la carga diferida de componentes al navegar dentro de la aplicación usando Vue Router:

js
// src/router/index.js

import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      name: 'home',
      component: HomeView
    },
    {
      path: '/about',
      name: 'about',
      // route level code-splitting
      // this generates a separate chunk (About.[hash].js) for this route
      // which is lazy-loaded when the route is visited.
      component: () => import('../views/AboutView.vue')
    }
  ]
})

export default router

Tal como indica el comment (que aparece por defecto al instalar Vue Router) al usar component: () => import('../views/AboutView.vue') en el bundle final este componente será generado dentro de un chunk (o sea, un archivo .js) aparte del resto del código JavaScript, y este chunk será cargado en forma lazy cuando el usuario visite esa ruta.

Esto hace que la carga inicial de la aplicación sea más rápida, porque no es necesario cargar ese componente al comienzo, pero también hace que la navegación hacia esa vista sea más lenta porque cuando Vue Router importa ese componente el browser tiene que parsear el código JS, interpretarlo, ejecutarlo... por eso no siempre es conveniente hacerlo de esta forma.

La forma más usual de hacerlo es importando el componente en forma anticipada, como es el caso del componente HomeView.vue en este ejemplo. Pero puede haber casos en que convenga usar lazy loading para componentes, por ejemplo, si son componentes muy grandes y pesados que retrasan la carga inicial 💡️