Cómo cargar imágenes de forma diferida en React sin afectar LCP
Para cargar una imagen de forma diferida en React, renderiza un elemento
img normal con loading="lazy". Mantén la URL real en
src, añade srcSet y
sizes cuando haya variantes adaptables, y proporciona
width y height para que el navegador pueda reservar
espacio.
La excepción importante es la imagen que probablemente determine el renderizado del mayor elemento con contenido (LCP) de la página. Mantén su carga inmediata y permite que se descubra en el HTML inicial. Cargarla de forma diferida retrasa la solicitud que la página más necesita completar rápidamente.
| Función de la imagen | Decisión de carga | Otros atributos importantes |
|---|---|---|
| Imagen principal o probable elemento LCP | Inmediata | fetchPriority="high" cuando se haya verificado |
| Otra imagen visible en el área de visualización inicial | Inmediata | Dimensiones y fuentes adaptables |
| Imagen de contenido normal fuera de pantalla | Diferida | src, srcSet, sizes y dimensiones |
| Fondo decorativo de CSS | Mecanismo independiente | No sustituyas contenido significativo de img |
«Visible sin desplazarse» no corresponde a un número fijo de imagen. La altura del área de visualización, los diseños adaptables, los banners y el contenido localizado pueden situar una misma imagen dentro o fuera del área de visualización inicial. Clasifica las imágenes según la página renderizada y luego verifica la elección en tamaños de pantalla representativos.
Usa la prop nativa de carga de React
React pasa la prop loading al elemento de imagen nativo del navegador. No
necesitas un efecto, un controlador de desplazamiento ni una biblioteca de Intersection Observer
para una imagen normal fuera de pantalla.
La siguiente vista previa local renderiza React en el servidor y envía al navegador el marcado completo de las imágenes. Puedes reutilizar los componentes en una aplicación React existente con renderizado en el servidor. La vista previa necesita Node.js y Corepack instalado por separado. Usa una versión de Node.js que siga recibiendo mantenimiento; esta vista previa se probó con Node.js 24.15.0 y 26.8.1, Yarn 4.12.0, React 19.2.6 y Chrome 151. Estas son las versiones probadas, no un requisito mínimo intrínseco para la carga nativa de imágenes. Los comandos usan un shell POSIX.
Crea un directorio react-image-preview nuevo y vacío, y abre una terminal allí. Guarda
package.json:
{
"name": "react-image-preview",
"private": true,
"type": "module",
"packageManager": "yarn@4.12.0",
"dependencies": {
"react": "19.2.6",
"react-dom": "19.2.6"
},
"devDependencies": {
"esbuild": "0.27.0"
}
}
Guarda .yarnrc.yml para seleccionar las dependencias locales e ignorar el
ejecutable de Yarn de un proyecto que contenga este directorio:
nodeLinker: node-modules
enableGlobalCache: false
ignorePath: true
Crea el archivo de bloqueo que delimita el proyecto antes de instalar, para que Yarn trate este directorio como un proyecto independiente:
touch yarn.lock && corepack yarn install
Si la instalación falla, resuelve el problema de red o del gestor de paquetes indicado y repite ese comando en el mismo directorio. Conserva tus archivos de código fuente.
Crea un directorio images con estos cuatro archivos WebP a partir de tus
propias fotos. Cada pareja debe mostrar el mismo recorte; los anchos de
srcSet deben coincidir con los anchos reales de los archivos en píxeles.
Cambia el texto alternativo para describir tus fotos.
| Archivo | Dimensiones en píxeles | Función |
|---|---|---|
images/harbor-640.webp | 640×360 | Candidata pequeña para imagen principal |
images/harbor-1280.webp | 1280×720 | Imagen principal grande y de respaldo |
images/workshop-480.webp | 480×320 | Candidata pequeña para imagen de contenido |
images/workshop-960.webp | 960×640 | Imagen de contenido grande y de respaldo |
Guarda este componente como ResponsiveImage.tsx:
import type { ReactNode } from 'react'
interface ResponsiveImageProps {
alt: string
height: number
sizes: string
src: string
srcSet: string
width: number
}
export function ResponsiveImage({
alt,
height,
sizes,
src,
srcSet,
width,
}: ResponsiveImageProps): ReactNode {
return (
<img
className="responsive-image"
src={src}
srcSet={srcSet}
sizes={sizes}
width={width}
height={height}
loading="lazy"
decoding="async"
alt={alt}
/>
)
}
Mantén una URL de imagen real en src. Mover la única URL a un atributo
personalizado data-src hace que la solicitud dependa de tu JavaScript e impide
que el escáner de precarga del navegador la descubra en el marcado normal de la imagen. También
deja la imagen sin cargar si el script falla.
La prop decoding="async" es una indicación independiente sobre la decodificación. No
determina cuándo comienza la solicitud de red ni sustituye a loading="lazy".
Guarda ImagePage.tsx. El gran espacio separador sitúa la imagen del taller muy
lejos del área de visualización inicial para este experimento de carga; no es un diseño que debas
copiar en tu aplicación.
import type { ReactNode } from 'react'
import { HeroImage } from './HeroImage.tsx'
import { ResponsiveImage } from './ResponsiveImage.tsx'
export function ImagePage(): ReactNode {
return (
<main>
<h1>React image preview</h1>
<section className="hero-column">
<HeroImage />
<p>The harbor photo loads with the page.</p>
</section>
<section className="demo-spacer">
<p>Scroll to the workshop photo.</p>
</section>
<section className="image-column">
<ResponsiveImage
src="/images/workshop-960.webp"
srcSet="/images/workshop-480.webp 480w, /images/workshop-960.webp 960w"
sizes="(max-width: 40rem) 100vw, 40rem"
width={960}
height={640}
alt="A technician calibrating a camera rig"
/>
<p>The workshop photo keeps this caption in place while it loads.</p>
</section>
</main>
)
}
La carga diferida solo cambia cuándo comienza la solicitud. No redimensiona, comprime, convierte, almacena en caché ni describe la imagen.
Mantén la carga inmediata de la imagen LCP
No apliques loading="lazy" a una imagen que probablemente sea el elemento LCP.
Renderiza su URL en el HTML inicial cuando tu framework de React admita el renderizado en el
servidor, para que el navegador pueda descubrirla sin esperar al renderizado del lado del cliente
ni a otra solicitud de datos.
Guarda HeroImage.tsx:
import type { ReactNode } from 'react'
export function HeroImage(): ReactNode {
return (
<img
className="responsive-image"
src="/images/harbor-1280.webp"
srcSet="/images/harbor-640.webp 640w, /images/harbor-1280.webp 1280w"
sizes="(max-width: 80rem) 100vw, 80rem"
width={1280}
height={720}
loading="eager"
fetchPriority="high"
alt="Fishing boats returning to the harbor at sunrise"
/>
)
}
fetchPriority="high" es una indicación de prioridad relativa, no una garantía sobre el
orden de las solicitudes. Resérvala para la imagen LCP identificada mediante mediciones. Si varias
imágenes tienen prioridad alta, el navegador dispone de menos información útil para decidir cuál
es la más importante.
React 19 también puede generar una precarga automática de imágenes durante el renderizado en el servidor. Inspecciona el HTML resultante antes de añadir una precarga manual: la documentación de imágenes de React describe las condiciones. Una precarga debe usar las mismas candidatas adaptables y el mismo tamaño del espacio de visualización que la imagen.
Una imagen puede ser el elemento LCP en dispositivos móviles y no en equipos de escritorio, o al revés. Usa datos de rendimiento de usuarios reales para confirmar qué elemento se convierte en el LCP de los visitantes reales, en lugar de asumir que todas las imágenes grandes o las diapositivas de un carrusel son críticas.
Combina la carga diferida con imágenes adaptables
Una solicitud retrasada aún puede descargar un archivo innecesariamente grande. Con descriptores
de ancho como 480w, srcSet proporciona los archivos
candidatos y sizes describe el ancho del espacio donde se muestra la
imagen. El navegador combina esa información con la densidad de píxeles del dispositivo y elige
una candidata.
Escribe sizes según el diseño, no según el archivo de origen. En esta vista
previa, la foto del taller ocupa el ancho del área de visualización hasta una columna de 40rem, y
la imagen principal lo ocupa hasta 80rem. El CSS de abajo hace efectivos esos límites. Con la
fuente raíz de 16 píxeles de la vista previa, los anchos en píxeles CSS son 640 y 1.280. Si tu aplicación
usa espaciado interior o un contenedor más estrecho, el valor de sizes
debe ser proporcionalmente menor.
El navegador realiza la misma selección de candidatas adaptables para imágenes de carga inmediata
y diferida. La decisión de carga no elimina la necesidad de contar con candidatas correctas y un
valor preciso de sizes. El navegador puede reutilizar una candidata más
grande almacenada en caché, y las pantallas de alta densidad pueden necesitar más píxeles de los
que proporcionan estas dos candidatas. Comprueba currentSrc con la caché vacía,
en lugar de considerar un nombre de archivo como una elección universal. Consulta la sección sobre
imágenes adaptables
para calcular el tamaño del espacio de visualización.
Reserva espacio para evitar cambios de diseño
Proporciona width y height para cada imagen de
contenido. Los navegadores usan esos atributos para calcular una relación de aspecto antes de que
se haya descargado la imagen, reservando la cantidad adecuada de espacio en el diseño. Tu CSS
puede seguir haciendo que la imagen tenga un tamaño flexible. Guarda preview.css:
html {
font-size: 16px;
color-scheme: light dark;
}
body {
margin: 0;
}
.responsive-image {
display: block;
width: 100%;
height: auto;
}
.hero-column {
max-width: 80rem;
margin-inline: auto;
}
.image-column {
max-width: 40rem;
margin-inline: auto;
}
.demo-spacer {
min-height: 6000px;
}
Los atributos deben describir la relación de aspecto intrínseca de la imagen. Por ejemplo, un
img puede usar width={960} y
height={640} cuando todas las candidatas tienen una relación de 3:2, incluso si
el navegador selecciona un archivo de 480×320. Si las fuentes con dirección artística usan
recortes o relaciones de aspecto diferentes, sus dimensiones deben describir la fuente
seleccionada, no una imagen de respaldo sin relación con ella.
El espacio reservado para las imágenes evita una fuente de cambios de diseño acumulativos (CLS), no todos los cambios posibles. Los pies de imagen, los anuncios, los controles de consentimiento, las fuentes y los mensajes de error aún pueden desplazar el contenido de la página.
Ejecuta la vista previa renderizada en el servidor
Guarda preview.tsx. Sirve únicamente esta página y los cuatro archivos conocidos
en tu interfaz de bucle local. Lee los archivos al iniciarse, por lo que una foto o una hoja de
estilos ausente detiene la vista previa antes de que indique una URL lista para usar. Esta vista
previa estática no incluye hidratación ni configuración para desplegarla en producción.
import { readFile } from 'node:fs/promises'
import { createServer } from 'node:http'
import { renderToStaticMarkup } from 'react-dom/server'
import { ImagePage } from './ImagePage.tsx'
async function main(): Promise<void> {
const images = new Map<string, Buffer>()
for (const name of [
'harbor-640.webp',
'harbor-1280.webp',
'workshop-480.webp',
'workshop-960.webp',
]) {
images.set(`/images/${name}`, await readFile(new URL(`./images/${name}`, import.meta.url)))
}
const css = await readFile(new URL('./preview.css', import.meta.url), 'utf8')
const html = '<!doctype html>' + renderToStaticMarkup(
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>React image preview</title>
<style>{css}</style>
</head>
<body><ImagePage /></body>
</html>,
)
const port = Number(process.env.PORT ?? 4173)
if (!Number.isInteger(port) || port < 0 || port > 65535) {
throw new Error('PORT must be an integer between 0 and 65535')
}
const server = createServer((request, response) => {
response.setHeader('Cache-Control', 'no-store')
const image = images.get(request.url ?? '')
if (image) {
response.writeHead(200, { 'Content-Type': 'image/webp' })
response.end(image)
} else if (request.url === '/') {
response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
response.end(html)
} else {
response.writeHead(404)
response.end('Not found')
}
})
server.on('error', (error) => {
console.error(`Could not start image preview: ${error.message}`)
process.exitCode = 1
})
process.once('SIGINT', () => { server.close() })
server.listen(port, '127.0.0.1', () => {
const address = server.address()
if (address && typeof address !== 'string') {
console.log(`Image preview: http://127.0.0.1:${address.port}/`)
}
})
}
main().catch((error: unknown) => {
console.error(error)
process.exitCode = 1
})
Compila y ejecuta desde el directorio de la vista previa. El ajuste vacío de
--tsconfig-raw impide que la configuración de TypeScript de un proyecto que
contenga este directorio cambie el entorno de ejecución JSX de esta vista previa. Esto reemplaza
el archivo generado preview.mjs; no modifica tus fotos. El
&& impide que una compilación fallida inicie un archivo generado
anteriormente.
corepack yarn exec esbuild preview.tsx --bundle --platform=node --format=esm --packages=external --jsx=automatic --tsconfig-raw='{}' --outfile=preview.mjs && node preview.mjs
Abre la URL que se muestra y desplázate hasta la foto del taller. Presiona Ctrl+C en la misma terminal para detener el servidor. Si al iniciarse se informa de un archivo ausente, corrige su nombre o ubicación y repite el comando de compilación y ejecución. Tras un error de puerto ocupado, elige un puerto disponible en un shell POSIX:
PORT=0 node preview.mjs
El servidor mostrará la nueva URL.
Cuándo conviene usar Intersection Observer
Usa el comportamiento de carga nativo del navegador para las imágenes normales. Recurre a
Intersection Observer solo cuando el componente necesite un comportamiento que
loading="lazy" no pueda expresar, como iniciar una animación, registrar un evento
de visibilidad o aplicar un fondo decorativo costoso poco antes de que llegue al área de
visualización.
No montes condicionalmente una imagen de contenido normal solo después de que se active un
observador, salvo que ese comportamiento sea realmente necesario. El montaje condicional retrasa
el descubrimiento, añade una posible vía de fallo de JavaScript y exige que diseñes una alternativa
estable. Las imágenes significativas deben seguir siendo elementos img
o picture con texto alternativo útil.
React.lazy() resuelve un problema diferente: difiere la carga del módulo de un
componente de JavaScript. No difiere automáticamente las solicitudes de imágenes que renderiza ese
componente.
Genera imágenes derivadas adaptables con Transloadit
La carga diferida no puede corregir una imagen de origen demasiado grande. Un pipeline práctico de entrega genera un conjunto pequeño y acotado de anchos, optimiza cada resultado, los exporta a un almacenamiento duradero y registra sus URL y dimensiones para la vista de React.
El Robot /image/resize de Transloadit puede crear variantes de ancho, y el Robot /image/optimize puede optimizar los formatos compatibles. Genera las variantes antes de renderizar la página, expórtalas a tu almacenamiento y registra sus dimensiones reales junto con sus URL duraderas. Una imagen de origen más pequeña que el ancho objetivo puede producir un resultado más pequeño; no etiquetes esa URL con un descriptor de ancho que no cumpla. Consulta el servicio de exportación de archivos para conocer las integraciones de almacenamiento, o Smart CDN para transformaciones bajo demanda.
La aplicación React sigue siendo responsable de las decisiones de presentación: qué imagen es significativa, su texto alternativo, el tamaño del espacio donde se muestra y si es crítica desde el inicio. Transloadit se encarga de la etapa de procesamiento de medios, en lugar de decidir qué imagen se convierte en el LCP en un diseño de página concreto.
Mide el resultado
Comprueba la página en producción en lugar de basarte únicamente en el código del componente:
- Inspecciona la respuesta HTML inicial. Los atributos
srcysrcsetde la imagen principal ya deberían estar presentes. - Registra cargas con áreas de visualización estrechas y amplias y la caché vacía en las herramientas de desarrollo del navegador. Confirma que la solicitud de la imagen principal comienza a partir de ese HTML, sin esperar al JavaScript de la aplicación.
- En la parte superior de la vista previa, comprueba que aún no se haya solicitado la foto lejana del taller. Desplázate hacia ella y observa cómo comienza su solicitud. La carga diferida nativa puede descargar la imagen antes de que sea visible.
- Inspecciona el
currentSrcde cada imagen y abre esa URL para comprobar las dimensiones del archivo en píxeles. El valornaturalWidthdel elemento puede estar corregido según la densidad. Compara el archivo seleccionado con el espacio donde se muestra y la relación de píxeles del dispositivo. Por ejemplo, en esta vista previa, Chrome con la caché vacía y una relación de píxeles de 1 seleccionó el archivo del taller de 480 píxeles en un área de visualización de 390 píxeles y el archivo de 960 píxeles para la columna de escritorio de 640 píxeles. - Retrasa las respuestas de las imágenes y compara el recuadro de la imagen y la posición del pie de imagen antes y después de la carga. Bloquea también las solicitudes para comprobar el texto alternativo y el espacio reservado cuando se produce un fallo.
Estas comprobaciones permiten determinar el comportamiento de descubrimiento, selección y diseño en una página local. No demuestran una mejora del LCP para los visitantes. En tu aplicación, identifica el elemento LCP real y mide el LCP y el CLS con datos de usuarios reales, agrupados por tipo de página y área de visualización pertinente. La guía de web.dev sobre LCP explica cómo el descubrimiento de recursos contribuye al retraso de carga.
Las distancias y la programación de la carga diferida del navegador dependen de su implementación. Evita reglas como «carga de forma diferida todo lo que venga después de la tercera imagen» o asumir un umbral fijo de píxeles. Mide las páginas y los dispositivos que tus usuarios realmente reciben.
Errores comunes de carga diferida en React
- Cargar de forma diferida la imagen principal o la imagen LCP identificada mediante mediciones.
- Mover la única URL de la imagen de
srcadata-src. - Renderizar imágenes críticas solo después de que termine el código del lado del cliente o una solicitud de datos.
- Proporcionar
srcSetsin un valor preciso desizes. - Omitir
widthyheightporque el CSS acaba controlando el tamaño de la imagen. - Marcar todas las imágenes con
fetchPriority="high". - Usar una biblioteca de observación solo para reproducir el comportamiento nativo del navegador.
- Asumir que
React.lazy()controla las solicitudes de red de las imágenes. - Diferir la carga de una imagen de origen de 3.000 píxeles en lugar de generar una imagen derivada adecuada.
Para conocer las reglas subyacentes del navegador, consulta el estándar de carga diferida de HTML. Con la ejecución de scripts desactivada, los navegadores cargan de inmediato estas imágenes con carga diferida nativa; mantener URL reales en el marcado sigue permitiendo que se cargue el contenido.
