¿Cómo implementar lazy loading en imágenes con Intersection Observer?
La implementación del lazy loading es una técnica eficiente que mejora el rendimiento de las aplicaciones web al carga imágenes solo cuando los usuarios las ven. Utilizar el Intersection Observer para esta tarea se convierte en una solución óptima por su capacidad de manejar múltiples entradas de forma eficiente y sencilla.
¿Cómo configurar el Intersection Observer?
Para implementar lazy loading en un proyecto, es esencial configurar el Intersection Observer correctamente. Un paso crucial es crear el observador antes de manejar elementos específicos, como las imágenes.
Callback Function: Se utiliza para manejar las acciones cada vez que un elemento entra o sale de la vista.
Entries: Representa los elementos individuales que el observador está siguiendo.
Observer: Sí, este objeto permite dejar de observar una imagen una vez se ha cargado.
¿Cómo estructurar tu código para lazy loading?
Organizar bien el código es clave para mantener la claridad y eficiencia. Adoptar una estructura modular puede facilitar la manipulación del DOM para el lazy loading.
Verificar la propiedad isIntersecting: Solo cargar la imagen cuando realmente está visible para el usuario.
¿Qué evitar al implementar lazy loading?
Al implementar esta funcionalidad, es conveniente mantener un equilibrio entre la optimización de carga y la experiencia del usuario:
Evita cargar todas las imágenes de golpe: Esta implementación se asegura de que únicamente las imágenes visibles sean cargadas.
Monitorear el CSS: Cuida que las imágenes tengan el tamaño adecuado inicialmente, para evitar que todas se visualicen incorrectamente.
Implementar lazy loading correctamente con Intersection Observer puede ahorrarte recursos, mejorar la experiencia del usuario y, sobre todo, asegurarte de que tu aplicación funcione de manera más eficiente. Si alguna técnica no encaja con tus necesidades, personalízala y compártela con la comunidad para seguir aprendiendo.
También si quieren evitar el problema que dice @JuanDC, en el que las imágenes cargan pequeñas, la manera de solucionarlo es eliminando el CLS (Culmulative Layout Shift - Cambio de diseño acumulativo), que básicamente es cuando un elemento mueve el diseño después de cargar el html base, y resulta molesto para los usuarios.
ㅤ
Para esto existen varias técnicas, la mejor para mí es: Teniendo en cuenta el aspect ratio de la imagen, agregar un width y height fijo y proporcional al aspect ratio, para que con unas propiedades, css pueda calcular su tamaño, y siempre mantenga la proporción, también sirve para agregar un skeleton loader responsive a las imágenes.
¿Qué es el aspect ratio?
ㅤ
¿Pero esto qué significa, y cómo se hace?
Por ejemplo, en las imágenes de nuestro proyecto, ya sea que pidamos una imagen de 200x300, 300x450 ... Etc. Estas imágenes (de los cards) siempre tendrán un aspec ratio de 2:3, ahora utilizaremos esto en código:
Para que CSS pueda calcular la altura, necesita las propiedades: aspect-ratio: n/n;, height: auto; y el width que necesitemos, en este caso puede ser 100%, porque en el código utilizo un grid
.img-responsive{aspect-ratio:2/3; // O también 200/300border-radius:1rem;height: auto;object-fit: cover;vertical-align: top;width:100%;}
También aquí hay información sobre el CLS, esta y otras técnicas para solucionarlo, en el que seguro lo explican más detallado, y mejor. 😅
ㅤ
Con esto, evitamos que cuando carguen las imágenes, muevan todo lo que está debajo de ellas, porque ya tienen su espacio asignado. 😃
Algo importante a tener en cuenta, es que solo funcionará al tener la imagen, para esto podemos utilizar alguna imagen de placeholder, utilizando algo como:
https://via.placeholder.com/200x300
Con esto podemos crear una función como:
exportconstLOADER_IMG=(w:number, h:number, color?:string, text?:string)=>`https://via.placeholder.com/${w}x${h}/${color}?text=${text}`;// Ejemplo de usoLOADER_IMG(200,300,"7a00f9","Test");// Url: https://via.placeholder.com/200x300/7a00f9?text=test
Toma tu estrellita por comentario / tutorial destacado: :star:
Existe una forma de hacer Lazy Loading con un atributo en la etiqueta img <img loading="lazy" />, esta sería una buena opción para hacer el Lazy Loading?
YEEEEES! Ahora también existe ese "atajo" y (lo que más me sorprende de todo) lo soportan la mayoría de navegadores web: https://twitter.com/Steve8708/status/1522628454906155008
.
¡Pero! Aunque este "atajo" es buenísimo y 100% recomendable, pueden existir otros casos donde utilizar el Intersection Observer nos ayude (y el atributo loading de las imágenes en HTML... pos no).
.
¿Se te ocurre algún ejemplo? :eyes:
Quizás Intersection Observer nos permite disparar alguna función de JS, o alguna animación al entrar el elemento o la sección a la pantalla?
Algo similar con el atributto loading, sin el intersectionObserver
movieImg.setAttribute('loading','lazy')
Probe esto pero igual me cargan todas las imágenes
Me di cuenta de que al igual que al profe Juan solo funciona en la pantalla principal, en las demás pantalla se ve que no funciona ni el método del profe ni el de loading "lazy" en el html.
Si las imagenes ya estan cargadas ya no es necesario el observer so...
antes del lazuLoading al verificar el performance de la aplicación con lighthouse mi rendimiento en mobile era del 3, después de implementarlo subió al51
Al final el profe dice:
si el height de las imágenes está en 0 no va a tomar el lazy
.
Esto es lo que pasa con las imágenes en la sección categoría. Por eso cargan todas al mismo tiempo.
.
Mi solución fue agregar esto al css
Lazy Loading (carga diferida en español) es una técnica de optimización utilizada en el desarrollo de aplicaciones web y móviles para retrasar la carga de recursos o elementos hasta que sean realmente necesarios. Esta técnica mejora el rendimiento y la experiencia del usuario al reducir el tiempo de carga inicial de la página o aplicación y el uso de ancho de banda. 🧑💻🧑💻
mi aporte es no usar la función setAttribute o getAttribute, ya el objeto target, tiene el path
const lazyLoader =newIntersectionObserver((entries)=>{ entries.forEach((entry)=>{if(entry.isIntersecting){const url = entry.target.getAttribute('data-img'); entry.target.setAttribute('src', url);// configuro el observador lazyLoader.unobserve(entry.target);}});});functioncreateMovies(movies, container, options ={}){// Desestructuramos los objetosconst{ useLazyLoader =false, observer =null}= options; container.innerHTML=''; movies.forEach(movie=>{if(movie.poster_path){const moviesContainer =document.createElement('div'); moviesContainer.classList.add('movie-container'); moviesContainer.addEventListener('click',()=>{location.hash=`#movie=${movie.id}`})const movieImg =document.createElement('img'); movieImg.setAttribute(lazyLoader ?'data-img':'src',`https://image.tmdb.org/t/p/w300${movie.poster_path}`); movieImg.classList.add('movie-img'); movieImg.setAttribute('alt', movie.title||'Sin título');if(useLazyLoader && observer){ lazyLoader.observe(movieImg);} moviesContainer.appendChild(movieImg); container.appendChild(moviesContainer);}else{console.log(`Película sin poster: ${movie.title}`);}});}exportasyncfunctiongetTrendingMoviesPreview(){try{const response =awaitfetch(proxyTrending);const data =await response.json();const movies = data.results;console.log("Películas recibidas:", movies);createMovies(movies, dom.trendingPreviewMovieList,{useLazyLoader:true,// Le damos el valor de true en el objetoobserver: lazyLoader // Mostramos la funcion del observer });}catch(error){console.error("Error al obtener datos:", error);}}
Estoy usando el Lazy loader tal cual y me carga todas las imagenes igualmente
let observer =newIntersectionObserver((entries)=>{ entries.forEach(entry=>{if(entry.isIntersecting){console.log("load")const urlImg = entry.target.getAttribute("data-img") entry.target.setAttribute("src",urlImg)}})});
Encima cada vez que muevo el carrusel de fotos, me vuelven a cargar las fotos y termino teniendo como 100 interaciones.
Ayudaa
¡Intenta ponerle un min-width y min-height por defecto a todas tus imágenes! Revisa y prueba si haciendo eso se soluciona. Si no, me cuentas y seguimos revisando. :D
existe alguna forma de hacer esto en react? o es de la misma forma mostrada aquí?
Como tengo un diseño diferente para las peliculas use 3 sliiders : use el siguiente código y me funciono:
// Creamos el observador con IntersectionObserver para cargar las imágenes al estar cerca al viweportconstcreateObserver=()=>{// Seleccionamos todas las imagenesconst img =document.querySelectorAll('img');// Configuración del IntersectionObserverconst options ={rootMargin:'200px',// Define un margen para el trigger antes de que la imagen entre al viewportthreshold:0.1,// Cuando el 10% de la imagen esté visible, se activará el callback};// Callback que se ejecuta cuando la imágen entra en el área visibleconstcallback=(entries, observer)=>{ entries.forEach(entry=>{// Si la imagen esta visibleif(entry.isIntersecting){const img = entry.target;const dataSrc = img.dataset.src;// Obtiene el data-src;// Carga la imagen y elimina el data-srcif(dataSrc){ img.src= dataSrc; img.removeAttribute('data-src');}// Deja de observar la imágen observer.unobserve(img);}})};// Crea y comienza a observar las imágenesconst observer =newIntersectionObserver(callback, options); img.forEach(img=> observer.observe(img));};// Ejecutamos la función una vez que el DOM esté completamente cargadodocument.addEventListener('DOMContentLoaded', createObserver);constgetMoviesProximmamente=async()=>{// Buscar el contenedor donde se muestran las peliculasconst movieContainer =document.querySelector('.movies-container-proximamente');if(!movieContainer){console.error('No se encontró el contenedor de peliculas.');return;}// Limpiar cualquier contenido previo y agregar el skeleton de carga movieContainer.innerHTML='';// Limpiar cualquier contenido previofor(let i =0; i <6; i++){// Crear 6 skeletons para simular las 6 películasconst loadingCard =document.createElement('div'); loadingCard.classList.add('loading-card-slider'); movieContainer.appendChild(loadingCard);}try{let{ data }=awaitapi('/movie/upcoming',{params:{language:'es'}});// Si no hay resultados en Español, intenta obtener en inglésif(!data.results|| data.results.length===0){({ data }=awaitapi('/movie/upcoming',{params:{language:'en-US'}}));}const movies = data.results;// Almacenar la respuesta las peliculas movieContainer.innerHTML='';// Limpiar cualquier contenido previo del contenedor// Mostrar las 6 primeras peliculas movies.slice(0,6).forEach(movie=>{// Limitar a 6 peliculasconst movieCard =document.createElement('div');// Crear el contenedor de cada película movieCard.classList.add('movie-card');// Agregar la clase 'movie-card'// Rellenar el contenedor con la imagen y título de la película movieCard.innerHTML=`<imgdata-src="https://image.tmdb.org/t/p/original${movie.poster_path}"alt="${movie.title}"><h6>${movie.title}</h6>`;// Agregar el evento de clic para actualizar el hash movieCard.addEventListener('click',()=>{location.hash='#movie'+ movie.id;// Actualiza el hash con el ID de la película});// Agregar la tarjeta de cada película al contenedor movieContainer.appendChild(movieCard);// 🔥 Llamamos a createObserver después de agregar las imágenes dinámicamentecreateObserver();});}catch(error){console.error('Ocurrió un problema:', error);// Si ocurrió un error mostrarlo en la consola}};getMoviesProximmamente();constgetPopularMovie=async()=>{// Buscar el contenedor donde se muestran las peliculasconst movieContainer =document.querySelector('.movies-container-pupular');if(!movieContainer){console.error('No se encontró el contenedor de peliculas.');return;}// Limpiar cualquier contenido previo y agregar el skeleton de carga movieContainer.innerHTML='';// Limpiar cualquier contenido previofor(let i =0; i <6; i++){// Crear 6 skeletons para simular las 6 películasconst loadingCard =document.createElement('div'); loadingCard.classList.add('loading-card-slider'); movieContainer.appendChild(loadingCard);}try{let{ data }=awaitapi('/movie/popular',{params:{language:'es'}});// Si no hay resultados en Español, intenta obtener en inglésif(!data.results|| data.results.length===0){({ data }=awaitapi('/movie/popular',{params:{language:'en-US'}}));}const movies = data.results;// Almacenar la respuesta las peliculas movieContainer.innerHTML='';// Limpiar cualquier contenido previo del contenedor// Mostrar las 6 primeras peliculas movies.slice(0,6).forEach(movie=>{// Limitar a 6 peliculasconst movieCard =document.createElement('div');// Crear el contenedor de cada película movieCard.classList.add('movie-card');// Agregar la clase 'movie-card'// Rellenar el contenedor con la imagen y título de la película movieCard.innerHTML=`<imgdata-src="https://image.tmdb.org/t/p/original${movie.poster_path}"alt="${movie.title}"><h6>${movie.title}</h6>`;// Agregar el evento de clic para actualizar el hash movieCard.addEventListener('click',()=>{location.hash='#movie'+ movie.id;// Actualiza el hash con el ID de la película});// Agregar la tarjeta de cada película al contenedor movieContainer.appendChild(movieCard);// 🔥 Llamamos a createObserver después de agregar las imágenes dinámicamentecreateObserver();});}catch(error){console.error('Ocurrió un problema:', error);// Si ocurrió un error mostrarlo en la consola}};getPopularMovie();constgetTredingMovies=async()=>{// Buscar el contenedor donde se muestran las peliculasconst movieContainer =document.querySelector('.movies-container-tedencias');if(!movieContainer){console.error('No se encontró el contenedor de peliculas.');return;}// Limpiar cualquier contenido previo y agregar el skeleton de carga movieContainer.innerHTML='';// Limpiar cualquier contenido previofor(let i =0; i <6; i++){// Crear 6 skeletons para simular las 6 películasconst loadingCard =document.createElement('div'); loadingCard.classList.add('loading-card-slider'); movieContainer.appendChild(loadingCard);}try{let{ data }=awaitapi('trending/movie/week',{params:{language:'es'}});// Si no hay resultados en Español, intenta obtener en inglésif(!data.results|| data.results.length===0){({ data }=awaitapi('trending/movie/week',{params:{language:'en-US'}}));}const movies = data.results;// Almacenar la respuesta las peliculas movieContainer.innerHTML='';// Limpiar cualquier contenido previo del contenedor// Mostrar las 6 primeras peliculas movies.slice(0,6).forEach(movie=>{// Limitar a 6 peliculasconst movieCard =document.createElement('div');// Crear el contenedor de cada película movieCard.classList.add('movie-card');// Agregar la clase 'movie-card'// Rellenar el contenedor con la imagen y título de la película movieCard.innerHTML=`<imgdata-src="https://image.tmdb.org/t/p/original${movie.poster_path}"alt="${movie.title}"><h6>${movie.title}</h6>`;// Agregar el evento de clic para actualizar el hash movieCard.addEventListener('click',()=>{location.hash='#movie'+ movie.id;// Actualiza el hash con el ID de la película});// Agregar la tarjeta de cada película al contenedor movieContainer.appendChild(movieCard);// 🔥 Llamamos a createObserver después de agregar las imágenes dinámicamentecreateObserver();});}catch(error){console.error('Ocurrió un problema:', error);// Si ocurrió un error mostrarlo en la consola}};
agregue una validacion hay un pequeño problema y es que igual se hace una petición aun cuando se carga la imagen