Brandon Argel Verdeja Domínguez
Brandon Argel Verdeja Domínguez
Juan Castro
ProfesorToma tu estrellita por comentario / tutorial destacado: :star:
Cargar todas las imágenes de una aplicación al mismo tiempo encarece la primera visita y degrada la experiencia. Con lazy loading usando IntersectionObserver puedes mostrar solo las imágenes visibles en el viewport, aligerar la carga inicial y aplicar el patrón de forma selectiva por sección.
¿Cómo crear el observador con IntersectionObserver?
La idea es centralizar un único observador reutilizable para todas las imágenes del DOM, sin pasar opciones extra, para mantener el código simple.
Dentro del archivo main.js, antes de las funciones de manipulación del DOM, defines el observador como un util perezoso. El nombre lazyLoader comunica mejor su propósito que llamarlo simplemente observer [02:00].
js const lazyLoader = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { const url = entry.target.getAttribute('data-img'); entry.target.setAttribute('src', url); } }); });
El callback recibe dos parámetros: entries y observer. En este caso solo necesitas el primero, porque la llamada a lazyLoader.observe() la haces fuera de la función [02:30].
¿Qué hace IntersectionObserver? Vigila elementos del DOM y dispara un callback cuando entran o salen del viewport, lo que te permite reaccionar solo a lo que el usuario realmente ve.
¿Por qué cambiar src por un atributo data-img?
Si dejas la URL en src desde el inicio, el navegador descarga la imagen aunque no esté visible. La técnica consiste en guardar la URL en un atributo personalizado y moverla a src solo cuando la imagen entra al viewport [04:00].
Dentro de createMovies modificas la asignación:
movieImage.setAttribute('src', urlImagen).movieImage.setAttribute('data-img', urlImagen).lazyLoader.observe(movieImage).Al recargar la página, el alt aparece visible porque el src aún no está. Eso confirma que la práctica de accesibilidad con texto alternativo funciona y que el navegador todavía no descarga la imagen [05:30].
¿Cómo acceder al elemento HTML real desde una entry?
Un detalle clave: cada entry no es el nodo HTML directamente, es una instancia con propiedades como isIntersecting, isVisible y target. El elemento real está en entry.target [08:00].
Por eso, métodos como getAttribute y setAttribute no existen en entry, sino en entry.target. La lógica final queda así:
entry.target.getAttribute('data-img').entry.target.setAttribute('src', url).if (entry.isIntersecting) para activar la carga solo cuando la imagen sea visible.¿Qué es isIntersecting? Es una propiedad booleana de cada entry que indica si el elemento observado está actualmente dentro del viewport o del contenedor configurado.
¿Cómo activar lazy loading solo en secciones específicas?
Aplicar la carga perezosa en toda la aplicación puede ser excesivo. En secciones como trending preview tiene sentido, pero en otras quizá quieras que las imágenes carguen de inmediato.
La solución es agregar un parámetro extra a createMovies que controle el comportamiento:
js function createMovies(movies, container, lazyLoad = false) { // ... if (lazyLoad) { movieImage.setAttribute('data-img', url); lazyLoader.observe(movieImage); } else { movieImage.setAttribute('src', url); } }
El parámetro lazyLoad es false por defecto, así que las llamadas existentes mantienen su comportamiento original. Solo donde quieras activar la optimización envías true, por ejemplo en getTrendingMoviesPreview [13:30].
Al recargar, la sección configurada con true muestra únicamente las tres primeras imágenes. El resto se carga conforme haces scroll, y puedes verificarlo en la pestaña Network del navegador, donde las peticiones aparecen escalonadas [14:00].
¿Qué errores comunes evitar al implementar lazy loading?
Hay un detalle de CSS que rompe todo el patrón sin que sea evidente. Si las imágenes tienen height: 0, el navegador las considera todas dentro del viewport simultáneamente y dispara la carga de golpe [15:30].
Puntos a cuidar:
height o min-height para los contenedores de imagen.target; nunca llames métodos del DOM directamente sobre la entry.Este patrón aligera la primera carga, mejora métricas de rendimiento y deja preparado el terreno para manejar imágenes rotas o null desde la API. ¿Tú lo resolviste igual o usaste otro enfoque? Cuéntalo en los comentarios.
Brandon Argel Verdeja Domínguez
Brandon Argel Verdeja Domínguez
Juan Castro
ProfesorToma tu estrellita por comentario / tutorial destacado: :star:
Orlando Díaz Torrealva
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?
Juan Castro
ProfesorYEEEEES! 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:
Lautaro Strappazzon
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')
Fabián Nava Pereda
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) }
Gilbert Ardila
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
Guadalupe Monge Barale
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; }
Ameth Ordoñez Erazo
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...
Guadalupe Monge Barale
Juan David Reyes
Me encanta la actitud de Juan los cursos aparte de interesantes y importantes, son divertidos n.n
Pedro Juan Guerrero Muñoz
Porque la sección de películas por categoría a pesar de que esta activado el lazyLoad siguen cargando todas las imágenes.
Juan Castro
ProfesorPor fa muéstrame tu código para ayudarte a revisar. :pray:
Guadalupe Monge Barale
Andrés Felipe Cubillos Ospina
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. 🧑💻🧑💻
Gonzalo Andrés Rojas Cardona
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; } }); });
Guadalupe Monge Barale
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
Johan Sebastian
Facundo Esteban Gasco
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
Juan Castro
Profesor¡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
Braulio Quezada
existe alguna forma de hacer esto en react? o es de la misma forma mostrada aquí?
Juan Castro
ProfesorPor supuesto que sí: https://platzi.com/clases/1601-react-avanzado/21268-usando-intersection-observer/
Braulio Quezada
gracias juan, grande como siempre 😎
Camilo Acosta
Cristian Mauricio Cavanzo Arias
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; } }); });
Ricardo Alfonso Chavez Vilcapoma
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.
Chema F
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í 😅
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
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
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; }
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); } }
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 } };