Como carregar imagens sob demanda no React sem prejudicar o LCP
Para carregar uma imagem sob demanda no React, renderize um elemento img
normal com loading="lazy". Mantenha a URL real em src,
adicione srcSet e sizes quando houver variantes
responsivas e forneça width e height para que o
navegador possa reservar espaço.
A exceção importante é a imagem que provavelmente será o elemento de Largest Contentful Paint (LCP) da página. Mantenha o carregamento dessa imagem imediato e permita que ela seja descoberta no HTML inicial. Carregá-la sob demanda atrasa a requisição que a página mais precisa concluir rapidamente.
| Função da imagem | Decisão de carregamento | Outra marcação importante |
|---|---|---|
| Imagem de destaque ou provável imagem de LCP | Imediato | fetchPriority="high" quando verificado |
| Outra imagem visível na área inicial da tela | Imediato | Dimensões e fontes responsivas |
| Imagem comum de conteúdo fora da tela | Sob demanda | src, srcSet, sizes e dimensões |
| Fundo decorativo em CSS | Mecanismo separado | Não substituir conteúdo significativo em img |
“Acima da dobra” não corresponde a um número fixo de imagens. A altura da área visível, os layouts responsivos, os banners e o conteúdo localizado podem fazer a mesma imagem aparecer dentro ou fora da área inicial da tela. Classifique as imagens com base na página renderizada e depois verifique a escolha em tamanhos de tela representativos.
Usar a prop nativa de carregamento do React
O React repassa a prop loading ao elemento de imagem nativo do navegador.
Você não precisa de um efeito, um manipulador de rolagem ou uma biblioteca de Intersection Observer
para uma imagem comum fora da tela.
A prévia local a seguir renderiza o React no servidor e envia a marcação completa das imagens ao navegador. Você pode reutilizar os componentes em uma aplicação React existente com renderização no servidor. A prévia precisa do Node.js e do Corepack instalado separadamente. Use uma versão do Node.js que ainda receba manutenção; esta prévia foi testada com Node.js 24.15.0 e 26.8.1, Yarn 4.12.0, React 19.2.6 e Chrome 151. Essas são versões testadas, não um requisito mínimo intrínseco para o carregamento nativo de imagens. Os comandos usam um shell POSIX.
Crie um diretório react-image-preview novo e vazio e abra um terminal nele.
Salve 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"
}
}
Salve .yarnrc.yml para selecionar dependências locais e ignorar o executável do
Yarn de um projeto que englobe esse diretório:
nodeLinker: node-modules
enableGlobalCache: false
ignorePath: true
Crie o arquivo de lock para delimitar o projeto antes de instalar, para que o Yarn trate este diretório como um projeto independente:
touch yarn.lock && corepack yarn install
Se a instalação falhar, corrija o problema de rede ou do gerenciador de pacotes informado e repita esse comando no mesmo diretório. Mantenha seus arquivos de código-fonte.
Crie um diretório images com estes quatro arquivos WebP a partir de suas
próprias fotos. Cada par deve mostrar o mesmo recorte; as larguras em srcSet
devem corresponder às larguras reais dos arquivos em pixels. Altere o texto alternativo para
descrever suas fotos.
| Arquivo | Dimensões em pixels | Função |
|---|---|---|
images/harbor-640.webp | 640×360 | Variante pequena da imagem de destaque |
images/harbor-1280.webp | 1280×720 | Imagem de destaque grande e alternativa padrão |
images/workshop-480.webp | 480×320 | Variante pequena da imagem de conteúdo |
images/workshop-960.webp | 960×640 | Imagem de conteúdo grande e alternativa padrão |
Salve 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}
/>
)
}
Mantenha uma URL real de imagem em src. Mover a única URL para um atributo
personalizado data-src faz a requisição depender do seu JavaScript e impede que
o scanner de pré-carregamento do navegador a descubra na marcação normal de imagens. Isso também
deixa a imagem sem carregar se o script falhar.
A prop decoding="async" é uma indicação separada sobre a decodificação. Ela não decide
quando a requisição de rede começa e não substitui loading="lazy".
Salve ImagePage.tsx. O espaçador grande coloca a imagem da oficina bem fora da área
inicial da tela para este experimento de carregamento; esse layout não deve ser copiado para sua
aplicação.
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>
)
}
O carregamento sob demanda só muda quando a requisição começa. Ele não redimensiona, comprime, converte, armazena em cache nem descreve a imagem.
Manter o carregamento da imagem de LCP imediato
Não aplique loading="lazy" a uma provável imagem de LCP. Renderize sua URL no HTML
inicial quando seu framework React oferecer renderização no servidor, para que o navegador possa
descobri-la sem esperar pela renderização no cliente ou por outra requisição de dados.
Salve 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" é uma indicação de prioridade relativa, não uma garantia da ordem das
requisições. Reserve-a para a imagem identificada como LCP nas medições. Se várias imagens forem
marcadas como de alta prioridade, o navegador terá menos informações úteis para decidir qual delas
é a mais importante.
O React 19 também pode emitir um pré-carregamento automático de imagem durante a renderização no servidor. Inspecione o HTML resultante antes de adicionar um pré-carregamento manual: a documentação de imagens do React descreve as condições. Um pré-carregamento deve usar as mesmas variantes responsivas e o mesmo tamanho do espaço ocupado pela imagem.
Uma imagem pode ser o elemento de LCP em dispositivos móveis, mas não em computadores, ou o contrário. Use dados de desempenho coletados em uso real para confirmar qual elemento se torna o LCP para visitantes reais, em vez de presumir que toda imagem grande ou slide de carrossel é crítico.
Combinar carregamento sob demanda com imagens responsivas
Uma requisição adiada ainda pode baixar um arquivo desnecessariamente grande. Com descritores de
largura como 480w, srcSet fornece os arquivos
candidatos e sizes descreve a largura do espaço renderizado. O navegador
combina essas informações com a densidade de pixels do dispositivo e escolhe um candidato.
Defina sizes para o layout, não para o arquivo de origem. Nesta prévia, a
foto da oficina preenche a área visível até o limite de uma coluna de 40rem, e a imagem de destaque
preenche essa área até 80rem. O CSS abaixo aplica esses limites. Com a fonte raiz de 16 pixels da
prévia, eles correspondem, em pixels CSS, a 640 e 1.280. Um preenchimento interno ou um contêiner mais
estreito na sua aplicação exige um valor proporcionalmente menor de sizes.
O navegador faz a mesma seleção de variantes responsivas para imagens com carregamento imediato e
sob demanda. A decisão de carregamento não elimina a necessidade de candidatos corretos e de um
valor preciso de sizes. O navegador pode reutilizar um candidato maior que
esteja em cache, e telas de alta densidade podem precisar de mais pixels do que essas duas variantes
oferecem. Verifique currentSrc com o cache vazio, em vez de tratar um único nome
de arquivo como uma escolha universal. Consulte imagens responsivas
para calcular o tamanho do espaço ocupado.
Reservar espaço para evitar mudanças no layout
Forneça width e height para cada imagem de conteúdo.
Os navegadores usam esses atributos para calcular a proporção antes que o download da imagem seja
concluído, reservando a quantidade certa de espaço no layout. Seu CSS ainda pode tornar a imagem
fluida. Salve 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;
}
Os atributos devem descrever a proporção intrínseca da imagem. Por exemplo, um
img pode usar width={960} e
height={640} quando todos os candidatos tiverem uma proporção de 3:2, mesmo que o
navegador selecione um arquivo de 480×320. Se fontes com direção de arte usarem recortes ou
proporções diferentes, suas dimensões precisam descrever a fonte selecionada, em vez de uma
alternativa padrão sem relação com ela.
O espaço reservado para a imagem evita uma fonte de Cumulative Layout Shift (CLS), não todas as mudanças possíveis. Legendas, anúncios, controles de consentimento, fontes e mensagens de erro ainda podem deslocar o conteúdo da página.
Executar a prévia renderizada no servidor
Salve preview.tsx. Ele serve apenas esta página e os quatro arquivos conhecidos
na sua interface de loopback. Os arquivos são lidos na inicialização, então uma foto ou folha de
estilos ausente interrompe a prévia antes que ela informe uma URL pronta para acesso. Esta prévia
estática não tem hidratação nem configuração de implantação em produção.
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
})
Compile e execute a partir do diretório da prévia. A configuração vazia --tsconfig-raw
impede que a configuração do TypeScript de um projeto que englobe esse diretório altere o runtime
JSX desta prévia. Isso substitui o preview.mjs gerado; não altera suas fotos.
O && impede que uma compilação com falha inicie um arquivo gerado antigo.
corepack yarn exec esbuild preview.tsx --bundle --platform=node --format=esm --packages=external --jsx=automatic --tsconfig-raw='{}' --outfile=preview.mjs && node preview.mjs
Abra a URL exibida e role até a foto da oficina. Pressione Ctrl+C no mesmo terminal para parar o servidor. Se a inicialização informar que falta um arquivo, corrija seu nome ou localização e repita o comando de compilação e execução. Após um erro de porta ocupada, escolha uma porta disponível em um shell POSIX:
PORT=0 node preview.mjs
O servidor exibirá a nova URL.
Quando o Intersection Observer é adequado
Use o comportamento nativo de carregamento do navegador para imagens comuns. Recorra ao
Intersection Observer apenas quando o componente precisar de um comportamento que
loading="lazy" não possa expressar, como iniciar uma animação, registrar um evento de
visibilidade ou aplicar um fundo decorativo de alto custo pouco antes de ele entrar na área visível.
Não monte condicionalmente uma imagem comum de conteúdo apenas depois que um observador for
acionado, a menos que esse comportamento seja realmente necessário. A montagem condicional atrasa
a descoberta, adiciona uma possibilidade de falha de JavaScript e exige que você projete uma
alternativa estável. Imagens significativas devem continuar sendo elementos
img ou picture com texto alternativo útil.
React.lazy() resolve outro problema: adia o carregamento do módulo JavaScript de
um componente. Ele não adia automaticamente as requisições das imagens renderizadas por esse
componente.
Gerar variantes responsivas com a Transloadit
O carregamento sob demanda não pode corrigir uma imagem de origem grande demais. Um fluxo prático de entrega gera um conjunto pequeno e limitado de larguras, otimiza cada resultado, exporta os arquivos para armazenamento durável e registra suas URLs e dimensões para a visualização no React.
O Robot /image/resize da Transloadit pode criar variantes de largura, e o Robot /image/optimize pode otimizar formatos compatíveis. Gere as variantes antes de renderizar a página, exporte-as para seu armazenamento e registre suas dimensões reais junto com suas URLs duráveis. Uma imagem de origem menor que uma largura de destino pode produzir um resultado menor; não rotule essa URL com um descritor de largura que ela não atende. Consulte o serviço de exportação de arquivos para integrações de armazenamento, ou o Smart CDN para transformações sob demanda.
A aplicação React continua responsável pelas decisões de apresentação: qual imagem é significativa, seu texto alternativo, o tamanho do espaço que ocupa na renderização e se ela é crítica inicialmente. A Transloadit cuida da etapa de processamento de mídia, em vez de decidir qual imagem se torna o LCP em um layout específico de página.
Medir o resultado
Verifique a página em produção, em vez de confiar apenas no código-fonte do componente:
- Inspecione a resposta HTML inicial. O
srce osrcsetda imagem de destaque já devem estar presentes. - Registre carregamentos em áreas visíveis estreitas e largas com o cache vazio nas ferramentas de desenvolvimento do navegador. Confirme que a requisição da imagem de destaque começa a partir desse HTML, sem esperar pelo JavaScript da aplicação.
- No topo da prévia, verifique se a foto distante da oficina ainda não foi solicitada. Role em sua direção e observe o início da requisição. O carregamento nativo sob demanda pode buscar a imagem antes que ela fique visível.
- Inspecione o
currentSrcde cada imagem e abra essa URL para verificar as dimensões do arquivo em pixels. OnaturalWidthdo elemento pode ter seu valor corrigido pela densidade. Compare o arquivo selecionado com o espaço renderizado e a razão de pixels do dispositivo. Por exemplo, nesta prévia, o Chrome com o cache vazio e uma razão de pixels de 1 selecionou o arquivo da oficina de 480 pixels em uma área visível de 390 pixels e o arquivo de 960 pixels para a coluna de 640 pixels em computadores. - Atrase as respostas das imagens e compare a posição da caixa da imagem e da legenda antes e depois do carregamento. Bloqueie também as requisições para verificar o texto alternativo e o espaço reservado em caso de falha.
Essas verificações confirmam o comportamento de descoberta, seleção e layout em uma página local. Elas não comprovam uma melhoria de LCP para os visitantes. Na sua aplicação, identifique o elemento real de LCP e meça LCP e CLS com dados coletados em uso real, agrupados por modelo de página e área visível relevante. As orientações de LCP do web.dev explicam como a descoberta de recursos contribui para o atraso no carregamento.
As distâncias e o agendamento do carregamento sob demanda do navegador dependem da implementação. Evite uma regra como “carregar sob demanda tudo depois da terceira imagem” ou presumir um limite fixo em pixels. Meça as páginas e os dispositivos que seus usuários realmente utilizam.
Erros comuns no carregamento sob demanda no React
- Carregar sob demanda a imagem de destaque ou a imagem identificada como LCP nas medições.
- Mover a única URL da imagem de
srcparadata-src. - Renderizar imagens críticas somente depois que a execução de código no cliente ou uma requisição de dados for concluída.
- Fornecer
srcSetsem um valor preciso desizes. - Omitir
widtheheightporque o CSS acabará controlando o tamanho da imagem. - Marcar todas as imagens com
fetchPriority="high". - Usar uma biblioteca de observadores apenas para reproduzir o comportamento nativo do navegador.
- Presumir que
React.lazy()controla as requisições de rede das imagens. - Adiar o carregamento de uma imagem de origem de 3.000 pixels em vez de gerar uma variante adequada.
Para conhecer as regras subjacentes do navegador, consulte o padrão HTML de carregamento sob demanda. Com a execução de scripts desativada, os navegadores carregam imediatamente essas imagens nativas configuradas para carregamento sob demanda; manter URLs reais na marcação ainda permite que o conteúdo seja carregado.
