José Luis Manco Velásquez
Nuevo chiste, ponerle un ! a mis amigos
Gagandeep Dass
yo en un examen de matematicas hallando la x e y: !
Cargar imágenes solo cuando aparecen en pantalla es una de las técnicas más efectivas para mejorar el rendimiento de una aplicación web. Con la web API Intersection Observer y un componente de React, puedes implementar lazy loading real, ahorrar peticiones HTTP y entregar una experiencia más fluida al usuario.
Esta guía te muestra cómo conectar un observador al DOM dentro de un componente de React con TypeScript, manejar el ciclo de vida con useEffect y cambiar dinámicamente el src de la imagen.
¿Qué es Intersection Observer y por qué usarlo en React?
El Intersection Observer es una web API que detecta cuándo un elemento del DOM entra o sale del viewport. En lugar de escuchar eventos de scroll (que son costosos), delega esa tarea al navegador y te avisa solo cuando hay un cambio relevante.
¿Qué hace Intersection Observer? Observa un nodo del DOM y ejecuta un callback cada vez que ese nodo cruza el límite visible de la pantalla. Es ideal para lazy loading, animaciones al hacer scroll o métricas de visibilidad.
En un componente de React, esta API te permite retrasar la carga de una imagen hasta que sea visible, evitando requests innecesarios. [00:24]
¿Cómo crear el observador dentro de un componente?
El flujo tiene tres pasos claros que debes ejecutar en orden dentro del componente. [01:08]
new IntersectionObserver.El constructor recibe un callback que entrega un array de entries. Cada entry trae la propiedad isIntersecting, que es true cuando el elemento ya entró al viewport.
tsx const observer = new window.IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { console.log('Hey, you!'); } }); });
Aquí TypeScript ayuda muchísimo: al escribir entry. te autocompleta las propiedades disponibles, así puedes programar casi sin abrir documentación. [02:52]
¿Por qué envolver todo dentro de useEffect?
En aplicaciones con server-side rendering como Next.js, el DOM no existe en el servidor. Por eso, el observador debe vivir en el cliente. La forma idiomática de garantizar eso en React es con useEffect y un array de dependencias vacío para que se ejecute una sola vez al montar el componente. [04:18]
Dentro del efecto debes verificar que node.current exista antes de llamar a observer.observe(node.current). Aunque parezca redundante, es la forma segura de evitar errores de TypeScript sin recurrir al operador bang (!).
¿Para qué sirve el operador ! en TypeScript? Le dice al compilador que confíe en ti y que el valor no será
nullniundefined. Funciona, pero es preferible usar una verificación explícita conifpara mantener la seguridad de tipos.
¿Cómo desconectar el observador correctamente?
Cada vez que un efecto crea una suscripción, debe limpiarla. Devuelve una función desde el useEffect que llame a observer.disconnect(). Así evitas fugas de memoria cuando el componente se desmonta o se re-renderiza. [05:30]
¿Cómo cambiar el src de la imagen solo cuando es visible?
Detectar la intersección es la mitad del trabajo. La otra mitad es renderizar la imagen real solo en ese momento. Para lograrlo, usa un estado local con useState que guarde el source actual.
isIntersecting sea true, actualiza el estado con la URL real recibida vía prop.src de la imagen.Como el efecto usa el prop image, debes incluirlo en el array de dependencias del useEffect. Es una buena práctica que React y TypeScript te van a recordar. [07:45]
Al probarlo en el navegador y filtrar por imágenes en la pestaña Network, verás que cada request HTTP ocurre solo cuando el zorro correspondiente entra al viewport. Antes de eso, no se descarga nada.
¿Cómo mejorar el placeholder con una imagen base64?
Dejar un espacio en blanco mientras la imagen carga genera una experiencia pobre. Un truco que se usa en el componente Image de Next.js es renderizar una imagen transparente en base64 de 320x320 píxeles y pintarla con CSS. [09:30]
Este detalle convierte un cuadro vacío en un skeleton funcional, lo que comunica al usuario que el contenido viene en camino.
Con el observador conectado, la referencia al nodo y el cambio dinámico de src, el componente RandomFox ya tiene la funcionalidad principal lista. ¿Qué le agregarías tú para hacerlo aún más reusable? Déjalo en los comentarios.
José Luis Manco Velásquez
Nuevo chiste, ponerle un ! a mis amigos
Gagandeep Dass
yo en un examen de matematicas hallando la x e y: !
un aporte, hay una libreria llamada react-lazy-load-image-component y es muy sencilla, se usa de la dsiguiente manera
npm install react-lazy-load-image-component
import {LazyLoadImage} from 'react-lazy-load-image-component'; import 'react-lazy-load-image-component/src/effects/blur.css
<LazyLoadImage className="w-full h-80 rounded-lg shadow-lg" src={image} alt={alt} effect="blur" />
Oscar Adrián Martínez Cavazos
Hola a todos devs!!. Les dejare un consejo de un problema que me paso a mi. Cuando cargaba varias imagenes con el boton y luego daba scroll hacia abajo veia que en mi Network se cargaban todas al mismo tiemo antes de hacer más scroll. . Me di cuenta de que era por que al no estar renderizadas las imagenes quedan todas juntas y al hacer scroll es como si todas aparecieran en la pantalla. . Lo arregle metiendo la imagen dentro de un div con margin y de esta manera ya existe una separacion entre las imagenes que no estan renderizadas aun.
Wilson Bienvenido Otaño Mateo
muy buen consejo gracias
Andres Fernando Saa Montaño
Sería buena idea ser explicitos también con el useState para que quede de la siguiente manera?
const [src, setSrc] = useState<string>("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIwIiBoZWlnaHQ9IjMyMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2ZXJzaW9uPSIxLjEiLz4=")
takeshi hp
Esta clase me encantó, gracias.
Sebastián Andrés Sanhueza Tapia
hoy en día también existe el atributo loading="lazy" que se puede colocar dentro de image y esta funcionalidad nativa de los navegadores permite que las imágenes e iframes se carguen solo cuando están cerca de entrar en la ventana visible (viewport)
export const RandomFoxLazy = ({ image, alt }: Props): JSX.Element => { return ( <img src={image} width={512} height="auto" alt={alt} className="rounded-xl m-4 bg-gray-300" loading="lazy" /> ); };
Se que el objetivo es aprender Ts con React, y para eso hacemos esta excelente clase, pero era un dato que les puede ser útil
Zaidibeth Ramos
En los recursos no hay nada, al menos en el nuevo frontend
Gilbert Ardila
! = ¡callate!, yo se qué estoy haciendo
David Rangel
Un buen material para poder estudiar las especificaciones: https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
Valentina Rodriguez Diaz
Harold Zurita Simon
Gonzalo Garcia-Ariño Merki
<img loading="lazy" /> //????
Jesus Heriberto Lara Juarez
un hechizo simple pero infalible
Francisco Alejandro Almanza Martínez
Si no tienes eslint activo,
usa en las dependencies del useEffect [setSrc, image]
Diego Reyes Cabrera
¿Por qué el profesor usó una imagen en base64 mientras que carga la imagen de la URL? ¿Alguien me explica? A mí me funciona igual solo especificando un width, height y background-color al elemento hasta que eventualmente cargue la imagen.
Andres Fernando Saa Montaño
Creo que son dos formas diferentes igualmente validas de implementar la misma funcionalidad
Jose Ever Muñoz Muñoz
excelente el truco de la img en base 64 ! lo voy a usar en todos mis proyectos !
Alejandro Calderón
Si quieren que la imagen solo sea visible si esta aparece completamente en el viewport, pueden agregar la opcion treshhold:
useEffect(() ``=>`` { ``let`` options = { threshold: 1.0 }; ``let`` observer = new IntersectionObserver((``entries``) ``=>`` { entries.forEach((``entry``) ``=>`` entry.isIntersecting && setSrc(image)); }, options); if (node.current) { observer.observe(node.current); }
return () ``=>`` observer.disconnect(); }, [image]);
Crees este custom hook para el observador y poderlo utilizar dentro del proyecto, hace uso tambien de la Web Api IntersectionObserver, y al ser un customHook, se puede retulizar de forma sencilla en la app.
import { type RefObject, useEffect, useState } from 'react' type ObserverProps = { ref: RefObject<HTMLElement | null> } /** * Hook personalizado para observar cuando un elemento entra o sale del viewport usando Intersection Observer. * @param {RefObject<HTMLElement | null>} ref - Ref del elemento a observar. * @returns {{ isIntersecting: boolean }} - Booleano que indica si el elemento es visible. */ export const useObserver = ({ ref }: ObserverProps) => { const [isIntersecting, setIsIntersecting] = useState(false) useEffect(() => { const observer = new IntersectionObserver(([entry]) => { if (entry.isIntersecting) { setIsIntersecting(true) observer.disconnect() } }) if (ref.current) { observer.observe(ref.current) } return () => { observer.disconnect() } }, [ref]) return { isIntersecting } }
Ejemplo de uso en un componente image:
const [status, setStatus] = useState<ImageStatus>('loading') const node = useRef<HTMLImageElement>(null) const { isIntersecting } = useObserver({ ref: node }) const imageSrc = isIntersecting || priority ? src : '' /** * Renderiza un fallback cuando la imagen no se puede cargar. * @returns {JSX.Element} Componente Skeleton. */ if (!src || status === 'error') { setStatus('error') return <Skeleton className="w-45 h-10" /> } return ( <img ref={node} src={imageSrc} alt={alt} aria-hidden={!priority} aria-label={priority ? alt : undefined} loading={priority ? 'lazy' : 'eager'} decoding={priority ? 'async' : 'sync'} role={priority ? 'img' : 'presentation'} width={100} height={100} className={classMerger( 'size-full object-contain object-center', className, )} /> )
🔹 Lazy Loading de Imágenes con IntersectionObserver
El objetivo de este patrón es cargar las imágenes solo cuando entran en el viewport, lo que mejora el rendimiento y reduce el consumo de datos. 💨📷
import { useRef, useEffect, useState } from "react"; type Props = { image: string; // URL real de la imagen }; export const RandomFox = ({ image }: Props): JSX.Element => { // 🔹 Referencia al elemento <img> const node = useRef<HTMLImageElement>(null); // 🔹 Estado del src de la imagen // Inicialmente usamos un placeholder en base64 (SVG vacío) const [src, setSrc] = useState( "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIwIiBoZWlnaHQ9IjMyMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2ZXJzaW9uPSIxLjEiLz4=" ); useEffect(() => { // 🔹 Creamos un IntersectionObserver const observer = new IntersectionObserver((entries) => { entries.forEach((entry) => { // 🔹 Cuando la imagen entra en el viewport if (entry.isIntersecting) { setSrc(image); // 📥 Cargamos la imagen real } }); }); // 🔹 Observamos el nodo si existe if (node.current) { observer.observe(node.current); } // 🔹 Limpiamos el observer al desmontar el componente return () => { observer.disconnect(); }; }, [image]); return ( <img ref={node} // 🔹 Referencia para el observer width={320} height="auto" src={src} // 🔹 Imagen placeholder o real className="rounded" /> ); };
🔹 Explicación paso a paso
useRef<HTMLImageElement>(null) nos permite acceder al <img> en el DOM y observar cuándo aparece en pantalla.src), evitando que el <img> aparezca vacío y mejorando la experiencia de usuario.<img> entra en el viewport.entry.isIntersecting es true cuando la imagen es visible.src al valor real (setSrc(image)), cargando la imagen solo cuando se necesita.observer.disconnect() al desmontar el componente para evitar fugas de memoria.🔹 Tip extra
Si quieres que la imagen solo se cargue una vez, puedes usar { once: true } en las opciones del observer:
const observer = new IntersectionObserver(callback, { threshold: 0.1 });
O incluso agregar un efecto de fade-in con CSS para que la carga se vea más suave ✨:
img { opacity: 0; transition: opacity 0.5s ease; } img[src*="data:image"] { opacity: 1; }