Dicas para reduzir layout shift (CLS) em páginas com conteúdo dinâmico
1. Entendendo o CLS e seu impacto no conteúdo dinâmico
Cumulative Layout Shift (CLS) é uma métrica do Core Web Vitals que mede a instabilidade visual de uma página. O cálculo considera dois fatores principais: a fração de impacto (quanto da tela foi afetada) e a distância do deslocamento (quanto o elemento se moveu). O Lighthouse calcula o CLS somando todas as mudanças inesperadas de layout durante o carregamento da página.
Páginas com conteúdo dinâmico são particularmente vulneráveis a altos CLS porque dependem de dados assíncronos que chegam em momentos imprevisíveis. APIs, carrosséis, anúncios e widgets de terceiros podem inserir elementos no DOM após o layout inicial já ter sido calculado, causando deslocamentos bruscos.
É importante distinguir entre layout shifts previsíveis (como fontes que trocam de tamanho ou imagens sem dimensões) e imprevisíveis (como dados que chegam via fetch e alteram a estrutura da página). Enquanto os primeiros podem ser resolvidos com técnicas como font-display: swap e atributos width/height, os segundos exigem estratégias mais elaboradas.
2. Reservando espaço para elementos carregados via JavaScript
A técnica mais eficaz para evitar CLS em conteúdo dinâmico é reservar espaço antecipadamente. Use contêineres com altura mínima para placeholders de dados assíncronos:
<div class="dynamic-content-container" style="min-height: 300px;">
<!-- Conteúdo será injetado aqui via JS -->
</div>
Para skeleton screens, aplique proporções fixas usando aspect-ratio:
.skeleton-card {
aspect-ratio: 16 / 9;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
Ao calcular dimensões reservadas, analise dados históricos da sua aplicação. Se uma listagem de produtos geralmente retorna entre 10 e 20 itens, calcule a altura média de cada card e multiplique pelo número esperado. Para APIs que retornam arrays de tamanho variável, considere usar max-height com overflow scroll como fallback:
.product-list-container {
min-height: 400px;
max-height: 800px;
overflow-y: auto;
}
3. Controlando a inserção de conteúdo assíncrono no DOM
A posição onde você insere elementos dinâmicos no DOM impacta diretamente o CLS. Inserir no meio do fluxo causa reflows em cascata. Prefira sempre anexar ao final do container pai:
// Ruim: insere no meio, causa reflow em todos os elementos seguintes
container.insertBefore(newElement, container.children[3]);
// Bom: insere ao final, afeta apenas o espaço reservado
container.appendChild(newElement);
Use content-visibility: auto para adiar o layout de blocos dinâmicos que estão fora da viewport:
.dynamic-section-offscreen {
content-visibility: auto;
contain-intrinsic-size: 500px;
}
Evite display: none para esconder placeholders, pois isso remove o elemento do fluxo de layout. Prefira visibility: hidden ou opacity: 0:
.placeholder-hidden {
visibility: hidden; /* Mantém o espaço reservado */
/* ou */
opacity: 0; /* Também mantém o espaço */
}
4. Gerenciamento de anúncios e widgets de terceiros
Anúncios são uma das maiores causas de CLS em sites reais. Defina slots com dimensões exatas:
.ad-slot {
width: 300px;
height: 250px;
position: relative;
overflow: hidden;
}
Implemente lazy loading para iframes e imagens de terceiros:
<iframe src="widget.html"
loading="lazy"
decoding="async"
width="560"
height="315">
</iframe>
Uma técnica avançada é atrasar scripts de terceiros até o layout principal estabilizar:
// Carrega scripts de terceiros apenas após o load principal
window.addEventListener('load', function() {
setTimeout(function() {
var adScript = document.createElement('script');
adScript.src = 'https://third-party-ad-network.com/ad.js';
document.body.appendChild(adScript);
}, 2000); // Aguarda 2 segundos após o load
});
5. Otimização de fontes e imagens para evitar reflows
Fontes personalizadas frequentemente causam CLS devido ao FOIT (Flash of Invisible Text) ou FOUT (Flash of Unstyled Text). Use font-display: swap com fallback de tamanho similar:
@font-face {
font-family: 'CustomFont';
src: url('custom-font.woff2') format('woff2');
font-display: swap;
}
body {
font-family: 'CustomFont', 'Arial', sans-serif;
/* Arial tem tamanho similar para minimizar shift */
}
Para imagens carregadas via API, sempre inclua atributos de dimensão mesmo com CSS responsivo:
<img src="dynamic-image.jpg"
width="800"
height="600"
style="max-width: 100%; height: auto;"
alt="Descrição">
Aplique aspect-ratio em contêineres de imagens dinâmicas:
.image-container {
aspect-ratio: 4 / 3;
overflow: hidden;
}
.image-container img {
width: 100%;
height: 100%;
object-fit: cover;
}
6. Monitoramento e debugging de CLS em produção
Use o Lighthouse para auditoria inicial e o Chrome DevTools para debugging detalhado. No DevTools, acesse Performance > Layout Shifts para ver um overlay visual dos deslocamentos:
Passos para identificar CLS no DevTools:
1. Abra DevTools (F12)
2. Vá para a aba "Performance"
3. Clique em "Record" e interaja com a página
4. Pare a gravação
5. Procure por "Layout Shift" na linha do tempo
6. Clique em cada shift para ver o elemento responsável
Configure a Web Vitals library para monitoramento em tempo real:
// Instalação via npm
npm install web-vitals
// Uso básico
import { getCLS } from 'web-vitals';
getCLS(console.log, { reportAllChanges: true });
No Search Console, configure alertas para CLS acima de 0.1 (limite recomendado pelo Google). Ferramentas de RUM como SpeedCurve ou Datadog podem fornecer dados agregados de usuários reais.
7. Boas práticas para conteúdo dinâmico com frameworks modernos
Em React e Vue, use chaves estáveis em listas renderizadas para evitar remontagens desnecessárias:
// React - use key estável (ID do banco, não índice)
{items.map(item => (
<div key={item.id}>{item.name}</div>
))}
Evite "layout thrashing" agrupando leituras e escritas no DOM:
// Ruim: leitura e escrita intercaladas
const height = element.offsetHeight; // Leitura
element.style.height = height + 'px'; // Escrita
const width = element.offsetWidth; // Leitura (força reflow)
element.style.width = width + 'px'; // Escrita
// Bom: agrupa leituras depois escritas
const height = element.offsetHeight;
const width = element.offsetWidth;
element.style.height = height + 'px';
element.style.width = width + 'px';
Implemente transições suaves usando propriedades que não causam reflow:
/* Ruim: causa reflow */
.element {
transition: height 0.3s, width 0.3s;
}
/* Bom: usa transform e opacity */
.element {
transition: transform 0.3s, opacity 0.3s;
}
Para animações de entrada de conteúdo dinâmico, use transform: translateY() em vez de modificar top ou margin-top:
@keyframes slideIn {
from {
transform: translateY(-20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.dynamic-item-enter {
animation: slideIn 0.3s ease-out;
}
Referências
- Cumulative Layout Shift (CLS) - Web.dev — Documentação oficial do Google sobre CLS, incluindo definição, cálculo e boas práticas
- Optimize Cumulative Layout Shift - web.dev — Guia prático com técnicas específicas para reduzir CLS em diferentes cenários
- CLS Debugging in Chrome DevTools - Chrome Developers — Tutorial detalhado sobre como usar o DevTools para identificar e corrigir layout shifts
- Web Vitals Library - GitHub — Biblioteca oficial do Google para medir Core Web Vitals em produção
- Preventing Layout Shifts with aspect-ratio - CSS-Tricks — Artigo técnico sobre o uso de aspect-ratio para evitar reflows em elementos dinâmicos
- Layout Thrashing Prevention - MDN Web Docs — Documentação sobre como evitar layout thrashing em JavaScript
- Font Display for Web Performance - Smashing Magazine — Guia completo sobre font-display e seu impacto no CLS