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:
<img className="img-responsive" alt="imagen" width="200" height="300" />
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 / 300 border-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:
export const LOADER_IMG = (w: number, h: number, color?: string, text?: string) => `https://via.placeholder.com/${w}x${h}/${color}?text=${text}`; // Ejemplo de uso LOADER_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...
if (entry.isIntersecting) { entry.target.setAttribute('src', url) lazyLoader.unobserve(entry.target) }
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
.genericList-container .movie-container .movie-img { height: 270px; min-height: 270px; max-height: 270px; width: 170px; min-width: 170px; max-width: 170px; }
Ya el CSS, tiene un ancho y alto definido (que no son las que tu usas, pero si ya las tiene), e igual no funciona...
En la clase siguiente se explica la solución
Estas clases de mi código no tenían esas medidas.
Pero me gusto más la solución propuesta por el profe
.movie-img { border-radius: 8px; min-height: 175px; }
Le damos un min-height a todas las imágenes para que el valor por defecto no sea 0 Con eso basta
Me encanta la actitud de Juan los cursos aparte de interesantes y importantes, son divertidos n.n
Porque la sección de películas por categoría a pesar de que esta activado el lazyLoad siguen cargando todas las imágenes.
Por fa muéstrame tu código para ayudarte a revisar. :pray:
Lo sulocioné así
Agregarle al css width y height, ya que el height estaba en 0 entonces no tomaba el lazy. Esto es lo que dice el profe al final
.genericList-container .movie-container .movie-img { height: 270px; min-height: 270px; max-height: 270px; width: 170px; min-width: 170px; max-width: 170px; }
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=new IntersectionObserver(entries=>{ entries.forEach(entry=>{ if (entry.isIntersecting) { entry.target.src=entry.target.dataset.src; } }); });
Le agregué que deje de escuchar el nodo (imagen) cuando ya la cargó. Es decir, que no la vuelva a cargar.
const lazyLoader = new IntersectionObserver((entries)=>{ entries.forEach((image)=>{ if (image.isIntersecting ) { const url = image.target.getAttribute('data-img'); image.target.setAttribute('src', url); lazyLoader.unobserve(image.target); } }); });
Seguí los pasos de esta clase
const lazyLoader = new IntersectionObserver((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); } }); }); function createMovies(movies, container, options = {}) { // Desestructuramos los objetos const { 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}`); } }); } export async function getTrendingMoviesPreview() { try { const response = await fetch(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 objeto observer: 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 = new IntersectionObserver((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í?
Por supuesto que sí: https://platzi.com/clases/1601-react-avanzado/21268-usando-intersection-observer/
gracias juan, grande como siempre 😎
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 viweport const createObserver = () => { // Seleccionamos todas las imagenes const img = document.querySelectorAll('img'); // Configuración del IntersectionObserver const options = { rootMargin: '200px', // Define un margen para el trigger antes de que la imagen entre al viewport threshold: 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 visible const callback = (entries, observer) => { entries.forEach(entry => { // Si la imagen esta visible if (entry.isIntersecting) { const img = entry.target; const dataSrc = img.dataset.src; // Obtiene el data-src; // Carga la imagen y elimina el data-src if (dataSrc) { img.src = dataSrc; img.removeAttribute('data-src'); } // Deja de observar la imágen observer.unobserve(img); } }) }; // Crea y comienza a observar las imágenes const observer = new IntersectionObserver(callback, options); img.forEach(img => observer.observe(img)); }; // Ejecutamos la función una vez que el DOM esté completamente cargado document.addEventListener('DOMContentLoaded', createObserver); const getMoviesProximmamente = async () => { // Buscar el contenedor donde se muestran las peliculas const 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 previo for (let i = 0; i < 6; i++) { // Crear 6 skeletons para simular las 6 películas const loadingCard = document.createElement('div'); loadingCard.classList.add('loading-card-slider'); movieContainer.appendChild(loadingCard); } try { let { data } = await api('/movie/upcoming', { params: { language: 'es' } }); // Si no hay resultados en Español, intenta obtener en inglés if (!data.results || data.results.length === 0) { ({ data } = await api('/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 peliculas const 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 = ` <img data-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ámicamente createObserver(); }); } catch (error) { console.error('Ocurrió un problema:', error); // Si ocurrió un error mostrarlo en la consola } }; getMoviesProximmamente(); const getPopularMovie = async () => { // Buscar el contenedor donde se muestran las peliculas const 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 previo for (let i = 0; i < 6; i++) { // Crear 6 skeletons para simular las 6 películas const loadingCard = document.createElement('div'); loadingCard.classList.add('loading-card-slider'); movieContainer.appendChild(loadingCard); } try { let { data } = await api('/movie/popular', { params: { language: 'es' } }); // Si no hay resultados en Español, intenta obtener en inglés if (!data.results || data.results.length === 0) { ({ data } = await api('/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 peliculas const 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 = ` <img data-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ámicamente createObserver(); }); } catch (error) { console.error('Ocurrió un problema:', error); // Si ocurrió un error mostrarlo en la consola } }; getPopularMovie(); const getTredingMovies = async () => { // Buscar el contenedor donde se muestran las peliculas const 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 previo for (let i = 0; i < 6; i++) { // Crear 6 skeletons para simular las 6 películas const loadingCard = document.createElement('div'); loadingCard.classList.add('loading-card-slider'); movieContainer.appendChild(loadingCard); } try { let { data } = await api('trending/movie/week', { params: { language: 'es' } }); // Si no hay resultados en Español, intenta obtener en inglés if (!data.results || data.results.length === 0) { ({ data } = await api('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 peliculas const 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 = ` <img data-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ámicamente createObserver(); }); } 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
const lazyLoader = new IntersectionObserver((entries) => { entries.forEach((entry) => { const element = entry.target; if (entry.isIntersecting && element.dataset['src'] && !element.src) { const url = element.dataset['src']; element.src = url; } }); });
Aquí una respusta a cual es mejor: lazo loading o intersectionObserver.
https://stackoverflow.com/questions/72847077/img-loading-lazy-vs-intersection-observer
RPTA: intersection observer da más control...pero ambas opciones hacen lo mismo.
Y si no lo resolvimos de ninguna manera? :'(
Llevo un tiempo ya haciendo los cursos de programación y la verdad, no se me habría ocurrido hacer todo esto.
F para mí 😅