Lazy loading de imagens e componentes: sem quebrar o LCP
Lazy loading bem feito acelera a página; mal feito piora o LCP. Quando usar loading=lazy, por que nunca no hero, como evitar CLS e fazer lazy de componentes.
Lazy loading virou sinônimo de "performance" e, por isso, gente coloca em tudo — inclusive onde não devia. O erro mais comum que eu vejo é aplicar loading="lazy" na imagem do hero, justamente o elemento que costuma ser o LCP da página. O resultado é o oposto do esperado: o browser adia o download da imagem mais importante, o LCP piora e a métrica que você queria melhorar vai pro buraco. Lazy loading é uma ferramenta cirúrgica, não um spray que você passa na página inteira.
Aqui eu vou direto ao ponto: quando adiar carregamento de verdade ajuda, quando atrapalha, e como fazer isso sem introduzir CLS nem aquele flash de espaço vazio que irrita o usuário. Vale pra imagens, iframes e componentes JavaScript.
O que lazy loading realmente faz
Lazy loading significa adiar o carregamento de um recurso até que ele esteja perto de ser necessário — normalmente quando entra (ou está prestes a entrar) na viewport. A ideia é simples: o que está abaixo da dobra não precisa baixar antes do que está visível. Você economiza banda, reduz o número de requests concorrendo no carregamento inicial e libera a rede pra priorizar o conteúdo que o usuário vê primeiro.
O ganho real aparece em páginas longas com muitas imagens: galerias, listagens de produto, artigos com dezenas de figuras. Numa página com 40 imagens, baixar só as 3 ou 4 visíveis no início é uma diferença enorme de tempo de carregamento e de dados móveis gastos. Mas o ganho some — ou vira prejuízo — quando você adia algo que o usuário precisa imediatamente.
Imagem nativa: loading="lazy"
Hoje não precisa de biblioteca pra fazer lazy de imagem. O atributo nativo resolve a maioria dos casos:
<img
src="/fotos/produto-12.webp"
alt="Tênis de corrida azul"
loading="lazy"
width="800"
height="600"
/>
O browser decide a hora certa de baixar com base na distância até a viewport, e ele faz isso melhor e mais barato do que qualquer JavaScript que você escreva. Suporte é universal em browsers modernos. Não tem desculpa pra carregar uma lib de "lazy load" de 8KB só pra isso.
NUNCA faça lazy na imagem do hero
Esse é o ponto que merece negrito. A imagem above-the-fold — o banner, o hero, a primeira figura visível — quase sempre é o elemento LCP. Se você marca ela como loading="lazy", o browser intencionalmente desprioriza o download dela. Você está dizendo "essa imagem pode esperar" sobre exatamente a imagem que define quão rápido a página parece carregar.
Pra o hero, faça o contrário: deixe loading="eager" (o padrão) e, se for o LCP, considere fetchpriority="high":
<img
src="/hero.webp"
alt="Painel principal do produto"
loading="eager"
fetchpriority="high"
width="1200"
height="630"
/>
A regra que eu sigo: eager para o hero, lazy por padrão abaixo da dobra. Se você não tem certeza se uma imagem está acima da dobra, ela provavelmente está perto demais pra valer o risco — deixe eager.
Reserve espaço pra não causar CLS
Lazy loading sem dimensão é uma fábrica de CLS (Cumulative Layout Shift). Quando a imagem só ocupa espaço depois de carregar, todo o conteúdo abaixo dela pula pra baixo no momento em que ela aparece. O usuário estava lendo, o layout se reorganiza, e ele perde a linha — ou pior, clica no lugar errado.
A correção é sempre reservar o espaço antes. O jeito mais robusto é declarar width e height no HTML: o browser calcula o aspect ratio e reserva a caixa mesmo antes do byte chegar.
<img src="/foto.webp" alt="..." loading="lazy" width="800" height="600" />
Quando a imagem é fluida e responsiva, use aspect-ratio no CSS:
img {
width: 100%;
height: auto;
aspect-ratio: 4 / 3;
}
Sem isso, cada imagem lazy que carrega empurra o resto da página e você acumula layout shift. CLS, LCP e INP andam juntos — se você quer entender como essas três métricas se relacionam e onde lazy loading entra em cada uma, vale ler o guia de Core Web Vitals.
IntersectionObserver pra casos custom
O atributo nativo cobre imagens e iframes. Quando você precisa adiar outra coisa — inicializar um mapa, montar um gráfico pesado, disparar uma animação só quando o bloco entra na tela — aí entra o IntersectionObserver. Ele te avisa quando um elemento cruza a viewport, sem você ficar amarrado a listener de scroll (que dispara dezenas de vezes por segundo e mata a performance).
const observer = new IntersectionObserver((entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
carregarConteudo(entry.target);
observer.unobserve(entry.target);
}
}
}, { rootMargin: '200px' });
document.querySelectorAll('[data-lazy]').forEach((el) => observer.observe(el));
O rootMargin: '200px' faz o carregamento começar 200px antes do elemento entrar na tela — assim o conteúdo chega "a tempo" em vez de aparecer com um flash atrasado. E sempre chame unobserve depois de carregar, pra não deixar observers pendurados consumindo CPU.
Lazy loading de componentes e code splitting
A mesma ideia vale pra JavaScript. Se um componente só aparece numa rota específica, ou só quando o usuário abre um modal, não faz sentido empacotá-lo no bundle inicial que toda visita baixa. O dynamic import quebra o código em chunks separados que só são buscados quando precisam:
button.addEventListener('click', async () => {
const Comp = await import('./Heavy');
montar(Comp.default);
});
O splitting por rota é o caso mais comum e o de maior retorno: cada página carrega só o JavaScript dela, e frameworks como SvelteKit, Next e Nuxt já fazem isso por padrão quando você usa o roteamento deles. Modais, customizers, editores ricos, bibliotecas de gráfico — tudo que 99% dos visitantes nunca abrem — deve ser import() dinâmico atado ao gatilho que realmente abre aquilo. O ganho aqui é direto no TBT e no INP: menos JavaScript pra parsear e executar no carregamento inicial significa main thread mais livre.
Iframes e os tradeoffs de adiar tudo
Iframes também aceitam o atributo nativo, e isso importa muito: um embed de YouTube ou de mapa carrega uma quantidade absurda de recursos de terceiros. Adiar isso até a viewport é um dos ganhos mais baratos que existem.
<iframe src="https://www.youtube.com/embed/..." loading="lazy" width="560" height="315"></iframe>
Mas — e esse "mas" é importante — não faça lazy em tudo. Cada elemento adiado tem um custo: um flash de espaço vazio enquanto o conteúdo chega, ou um pequeno atraso perceptível quando o usuário rola rápido. Adiar um botão crítico, um texto importante ou um componente que o usuário vai ver de imediato é trocar uma métrica por uma experiência pior. Lazy loading bem feito é invisível; mal feito, o usuário sente o site "montando" embaixo dele.
E tem um limite que lazy loading nenhum resolve: ele não conserta uma imagem de 4MB. Adiar um arquivo gigante só adia a lentidão — quando ele finalmente carrega, ainda trava a conexão e demora. Comprimir as imagens antes é metade da solução; o compressor de imagens corta o peso antes de você sequer pensar em adiar o carregamento. Imagem leve mais lazy abaixo da dobra é a combinação que funciona.
Perguntas frequentes
Lazy loading melhora ou piora o LCP?
Depende de onde você aplica. Em imagens abaixo da dobra, melhora indiretamente, porque libera banda pra o conteúdo visível baixar primeiro. Na imagem do hero ou em qualquer coisa above-the-fold, piora — você adia justamente o elemento que define o LCP. A regra é eager pro hero, lazy pro resto.
Preciso de biblioteca pra fazer lazy loading?
Pra imagens e iframes, não. O atributo nativo loading="lazy" é suportado por todos os browsers modernos e faz o trabalho melhor do que qualquer lib JavaScript. Só recorra a IntersectionObserver quando precisar adiar algo que não seja imagem ou iframe — inicializar um gráfico, montar um componente, disparar uma animação.
Como o lazy loading causa layout shift?
Quando a imagem não tem dimensão reservada, ela ocupa zero espaço até carregar e depois empurra todo o conteúdo abaixo. Esse pulo é CLS. A correção é declarar width e height no HTML, ou definir aspect-ratio no CSS, pra o browser reservar a caixa antes mesmo da imagem chegar.
Devo fazer lazy loading em todos os componentes?
Não. Faça lazy só no que a maioria dos visitantes não usa de imediato — modais, editores pesados, gráficos, embeds. Componentes que aparecem no carregamento inicial ou logo no topo devem vir no bundle principal, porque adiá-los só adiciona atraso perceptível sem ganho real.
O resumo que importa
Lazy loading é uma faca afiada: cortou no lugar certo, acelera a página; cortou no lugar errado, machuca o LCP e a experiência. Eager pro hero, lazy por padrão abaixo da dobra. Sempre reserve o espaço com width/height ou aspect-ratio pra não gerar CLS. Use o atributo nativo pra imagem e iframe, IntersectionObserver pros casos custom, e dynamic import pra adiar JavaScript que ninguém pediu ainda. E nunca esqueça do óbvio: comprima as imagens antes — adiar um arquivo pesado não o torna leve.
- 01 O que é CDN: por que ela acelera sites no mundo inteiro Entenda o que é uma CDN, como edge servers e cache reduzem latência e por que ela acelera sites no mundo inteiro — além de DDoS, WAF e TLS.
- 02 Salt, pepper, bcrypt e Argon2id: como proteger senhas de verdade Em 2012, o LinkedIn expôs 117mi de senhas. SHA-1 sem salt — 90% quebradas em 4h. Entenda o que cada camada de proteção resolve e por que Argon2id é a escolha certa hoje.