Capa do Ebook gratuito Performance Front-End: Otimizando Core Web Vitals sem Mistério

Performance Front-End: Otimizando Core Web Vitals sem Mistério

Novo curso

19 páginas

Prevenção de CLS: dimensionamento, reserva de espaço e padrões de layout estáveis

Capítulo 15

Tempo estimado de leitura: 12 minutos

+ Exercício

O que é CLS e por que ele acontece na prática

CLS (Cumulative Layout Shift) mede a soma dos “saltos” inesperados de layout que ocorrem durante o ciclo de vida da página. Um salto acontece quando um elemento visível muda de posição entre dois frames sem que isso seja consequência direta de uma interação do usuário (por exemplo, clicar em um botão que abre um acordeão). Na experiência real, CLS aparece como texto que “desce”, botões que mudam de lugar no momento do clique, cards que se reorganizam, ou banners que empurram o conteúdo para baixo.

Na prática, quase todo CLS vem de uma causa simples: o navegador precisou renderizar algo sem saber o tamanho final de algum conteúdo, e quando esse conteúdo chega (imagem, iframe, anúncio, bloco de recomendação, fonte, componente hidratado), ele ocupa espaço e empurra o restante. A prevenção de CLS, portanto, é principalmente um trabalho de: (1) dimensionar elementos antes de carregarem, (2) reservar espaço para conteúdo que chega depois, e (3) adotar padrões de layout que não dependam de “surpresas” de tamanho.

Princípios de prevenção: dimensionamento, reserva e estabilidade

1) Dimensionamento explícito: o navegador precisa saber o espaço antes

Quando você informa dimensões (ou uma razão de aspecto), o browser consegue calcular o layout inicial com previsibilidade. Isso reduz a chance de reflow quando o recurso termina de carregar. O objetivo é que o layout “final” seja praticamente o mesmo layout “inicial”, com apenas a troca de conteúdo (por exemplo, placeholder vira imagem) sem deslocar o restante.

2) Reserva de espaço: placeholders e esqueletos com medidas reais

Nem sempre você sabe o tamanho exato do conteúdo, mas você pode reservar um espaço aproximado ou um espaço mínimo garantido. O ponto crítico é que o placeholder deve ocupar o mesmo espaço do conteúdo final. Um skeleton menor do que o card final, por exemplo, ainda causa shift quando o card real entra.

3) Padrões de layout estáveis: evitar mudanças estruturais

Mesmo com dimensões, alguns padrões de layout são naturalmente instáveis: inserir elementos no topo, trocar classes que mudam altura, alternar entre layouts com e sem barra lateral, ou depender de conteúdo assíncrono para definir a grade. Padrões estáveis priorizam: (a) reservar “slots” fixos para conteúdo dinâmico, (b) evitar inserir acima do conteúdo já renderizado, e (c) preferir animações por transform/opacity em vez de alterar propriedades que recalculam layout.

Continue em nosso aplicativo

Você poderá ouvir o audiobook com a tela desligada, ganhar gratuitamente o certificado deste curso e ainda ter acesso a outros 5.000 cursos online gratuitos.

ou continue lendo abaixo...
Download App

Baixar o aplicativo

Fontes comuns de CLS (sem repetir diagnóstico) e como atacar cada uma

Imagens sem dimensões e mídia incorporada

Imagens e vídeos são causas clássicas de CLS quando o HTML/CSS não define o espaço. O browser só descobre o tamanho depois que baixa metadados ou o recurso completo, e então ajusta o layout.

  • Defina width e height no elemento (ou use aspect-ratio no CSS).
  • Garanta que o container tenha dimensões estáveis em todos os breakpoints.
  • Para iframes (maps, players, embeds), use wrappers com razão de aspecto.
<!-- Dimensões explícitas ajudam o browser a reservar espaço -->
<img src="/media/produto.jpg" width="640" height="480" alt="Produto" />

<!-- Para vídeo/iframe, use um wrapper com aspect-ratio -->
<div class="embed">
  <iframe src="https://player.example.com/123" loading="lazy"></iframe>
</div>

/* CSS */
.embed {
  aspect-ratio: 16 / 9;
  width: 100%;
}
.embed iframe {
  width: 100%;
  height: 100%;
  border: 0;
}

Conteúdo injetado tardiamente (banners, avisos, recomendações)

Componentes que aparecem após a primeira renderização (por resposta de API, personalização, A/B test, consentimento, etc.) tendem a empurrar o conteúdo se forem inseridos “no fluxo” sem espaço reservado.

  • Crie um “slot” fixo no layout para esse componente, com altura mínima.
  • Se o componente for opcional, mantenha o slot com um placeholder colapsável apenas quando houver interação do usuário (por exemplo, “ver mais”).
  • Evite inserir banners no topo do conteúdo já renderizado; prefira overlay (sem empurrar) quando apropriado e acessível.
<!-- Slot reservado para um banner dinâmico -->
<section class="promo-slot" aria-label="Promoções">
  <div class="promo-skeleton"></div>
</section>

/* Reserva de espaço: evita empurrar o conteúdo */
.promo-slot {
  min-height: 120px; /* baseado no tamanho real do banner */
}
.promo-skeleton {
  height: 120px;
  background: #eee;
  border-radius: 12px;
}

Anúncios e widgets de terceiros

Ads são uma das maiores fontes de CLS porque o tamanho final pode variar por leilão, criativo e dispositivo. Além disso, scripts de terceiros podem inserir iframes e containers com alturas dinâmicas.

  • Reserve espaço com tamanhos conhecidos (por breakpoint) e use containers com dimensões fixas.
  • Quando houver múltiplos tamanhos possíveis, reserve o maior (ou um tamanho “seguro”) para evitar expansão.
  • Evite “colapsar” o container após falha de preenchimento; prefira manter o espaço ou colapsar apenas após interação/rolagem, dependendo do caso de uso.
<!-- Exemplo de container de anúncio com tamanhos por viewport -->
<div class="ad-slot">
  <!-- script do ad injeta aqui -->
</div>

.ad-slot {
  width: 100%;
  min-height: 250px; /* mobile padrão */
}
@media (min-width: 768px) {
  .ad-slot { min-height: 280px; }
}
@media (min-width: 1024px) {
  .ad-slot { min-height: 300px; }
}

Componentes hidratados (SSR/SSG + JS) que mudam o layout

Em aplicações com renderização no servidor, o HTML inicial pode ser estável, mas a hidratação no cliente pode alterar o DOM: trocar texto, inserir ícones, mudar espaçamentos, recalcular alturas, ou aplicar estilos que não estavam presentes no primeiro paint. Isso gera shifts “tardios”.

  • Garanta que o HTML inicial e o estado inicial no cliente sejam equivalentes (mesmos nós, mesmas classes, mesmas dimensões).
  • Evite renderizar “vazio” no servidor e preencher depois; prefira renderizar skeleton com dimensões finais.
  • Se um componente depende de dados do cliente (ex.: geolocalização), reserve um slot com tamanho estável e substitua o conteúdo sem alterar a altura.
// Exemplo conceitual (pseudo): manter altura estável durante hidratação
function WeatherWidget() {
  return (
    <div className="weather">
      <div className="weather__slot">
        {/* server: skeleton; client: conteúdo real */}
      </div>
    </div>
  );
}

/* CSS: slot com altura fixa */
.weather__slot { min-height: 64px; }

Passo a passo prático: checklist para “blindar” uma página contra CLS

Passo 1: Liste tudo que carrega depois do HTML inicial

Faça um inventário dos elementos que podem aparecer ou mudar após o primeiro render: imagens, iframes, anúncios, banners de consentimento, toasts, recomendações, comentários, componentes de personalização, e qualquer bloco condicionado por API. O objetivo é identificar onde você precisa reservar espaço.

  • Quais blocos são assíncronos?
  • Quais blocos têm altura variável?
  • Quais blocos podem aparecer acima da dobra?

Passo 2: Para cada mídia, defina dimensões ou razão de aspecto

Para imagens, use width/height ou aspect-ratio. Para cards com imagem no topo, a imagem deve ter altura previsível. Para embeds, use wrappers com aspect-ratio.

/* Card com mídia estável */
.card__media {
  aspect-ratio: 4 / 3;
  width: 100%;
  overflow: hidden;
  border-radius: 12px;
}
.card__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

Se a imagem tiver proporções diferentes por item, ainda dá para estabilizar: escolha uma proporção padrão para o layout (por exemplo, 1:1 em grid de produtos) e use object-fit: cover para recorte.

Passo 3: Crie “slots” com min-height para conteúdo dinâmico

Para qualquer bloco que pode aparecer depois, crie um container com altura mínima baseada no tamanho final esperado. Se houver variação, use o pior caso (maior altura) ou defina limites e trate overflow internamente.

<main>
  <section class="recommendations">
    <h2>Recomendados</h2>
    <div class="recommendations__slot">
      <!-- skeleton inicial -->
      <ul class="skeleton-grid">
        <li></li><li></li><li></li><li></li>
      </ul>
    </div>
  </section>
</main>

.recommendations__slot {
  min-height: 360px; /* altura do grid final */
}
.skeleton-grid {
  list-style: none;
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 12px;
  padding: 0;
  margin: 0;
}
.skeleton-grid li {
  height: 168px;
  background: #eee;
  border-radius: 12px;
}

Passo 4: Evite inserir conteúdo acima do que já está visível

Inserir um elemento no topo do conteúdo (por exemplo, um aviso que aparece depois) é quase garantia de shift. Prefira uma destas abordagens:

  • Slot pré-reservado no topo: já existe um espaço para o aviso, mesmo vazio.
  • Overlay: o aviso aparece sobre o conteúdo sem empurrar (com cuidado de acessibilidade e sem bloquear ações essenciais).
  • Inserção abaixo da dobra: se o conteúdo não é crítico, adie para uma área menos sensível.
<!-- Slot no topo já reservado -->
<div class="top-notice-slot"></div>

.top-notice-slot {
  min-height: 56px; /* altura do aviso */
}

Passo 5: Trate estados de carregamento para não mudar altura

Estados “loading” e “empty” precisam manter a mesma estrutura do estado final. Erros comuns: spinner pequeno substituindo uma lista grande; texto “carregando” em uma linha substituindo um card alto; ou esconder o container inteiro até a API responder.

  • Use skeletons com dimensões equivalentes ao conteúdo final.
  • Mantenha o container no DOM e troque apenas o conteúdo interno.
  • Evite alternar entre display: none e display: block em blocos grandes sem reserva de espaço.
/* Em vez de esconder tudo, mantenha o bloco com altura mínima */
.results {
  min-height: 480px;
}
.results.is-loading .results__content {
  visibility: hidden; /* mantém o espaço */
}
.results.is-loading .results__skeleton {
  display: block;
}
.results__skeleton {
  display: none;
}

Passo 6: Use animações que não causem reflow

Quando precisar animar entrada/saída, prefira transform e opacity. Evite animar height, top, margin e padding em elementos que empurram outros, pois isso recalcula layout e pode contribuir para shifts perceptíveis.

/* Entrada suave sem empurrar layout */
.toast {
  position: fixed;
  right: 16px;
  bottom: 16px;
  transform: translateY(16px);
  opacity: 0;
  transition: transform 200ms ease, opacity 200ms ease;
}
.toast.is-visible {
  transform: translateY(0);
  opacity: 1;
}

Padrões de layout estáveis para componentes comuns

Header fixo e barras que aparecem/desaparecem

Headers que mudam de altura ao carregar (por exemplo, quando o menu vira “sticky” ou quando um banner entra) podem deslocar o conteúdo. Um padrão estável é definir a altura do header desde o início e, se houver variações internas, resolver dentro do próprio header (com overflow/scroll interno ou slots).

  • Defina min-height do header igual ao estado final.
  • Se existir uma faixa promocional, reserve o espaço dela mesmo quando vazia.
header.site-header {
  min-height: 72px;
}
.header-promo {
  min-height: 32px;
}

Grids de cards com conteúdo variável

Descrições com tamanhos diferentes, badges condicionais e preços com variações podem alterar a altura dos cards e causar reorganização visual (especialmente em grids). Para estabilizar:

  • Padronize alturas de áreas internas (título, preço, ações) com linhas limitadas.
  • Use line-clamp para limitar títulos longos.
  • Reserve espaço para badges mesmo quando não existirem (slot).
.card__title {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  min-height: 2.6em; /* reserva para 2 linhas */
}
.card__badge-slot {
  min-height: 20px;
}

Tabelas e listas que recebem dados depois

Quando uma tabela aparece vazia e depois “cresce”, o usuário percebe deslocamento. Um padrão estável é renderizar a estrutura completa (cabeçalho, linhas skeleton) com alturas fixas e substituir célula a célula.

<table class="orders">
  <thead>...</thead>
  <tbody>
    <tr class="row-skeleton"><td></td><td></td><td></td></tr>
    <tr class="row-skeleton"><td></td><td></td><td></td></tr>
    <tr class="row-skeleton"><td></td><td></td><td></td></tr>
  </tbody>
</table>

.orders td { height: 44px; }
.row-skeleton td { background: #eee; }

Casos especiais: quando o tamanho é desconhecido

Conteúdo gerado pelo usuário (UGC) e textos imprevisíveis

Comentários, avaliações e descrições podem variar muito. Para evitar que uma área “exploda” de tamanho após carregamento:

  • Defina um max-height com “ver mais” (interação do usuário) para expandir.
  • Use placeholders com altura equivalente ao máximo inicial exibido.
  • Evite inserir imagens inline sem dimensões; trate-as como mídia com razão de aspecto.
.comment {
  max-height: 160px;
  overflow: hidden;
}
.comment.is-expanded {
  max-height: none;
}

Personalização e experimentos

Quando A/B tests trocam componentes, o layout pode mudar entre usuários e até durante a sessão. Um padrão seguro é manter a mesma “caixa” (mesma altura/largura) e variar apenas o conteúdo interno. Se variantes tiverem alturas diferentes, escolha uma altura comum (a maior) e alinhe internamente.

.hero-variant-slot {
  min-height: 320px; /* maior entre variantes */
  display: grid;
  align-items: center;
}

Boas práticas rápidas (para aplicar em PRs e code review)

  • Todo <img> deve ter width e height ou estar dentro de um container com aspect-ratio.
  • Todo <iframe> deve estar em wrapper com tamanho previsível.
  • Todo bloco assíncrono acima da dobra deve ter slot reservado com min-height.
  • Evite inserir conteúdo no topo após render; prefira slot ou overlay.
  • Skeletons devem replicar dimensões do layout final (não apenas “algo cinza”).
  • Evite alternar layouts inteiros após hidratação; mantenha estrutura e classes consistentes.
  • Prefira animações com transform/opacity para não provocar reflow.

Agora responda o exercício sobre o conteúdo:

Qual abordagem mais reduz o risco de CLS quando um componente dinâmico (como banner ou recomendações) é inserido após a primeira renderização?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

CLS costuma ocorrer quando o conteúdo chega sem espaço reservado e empurra o restante. Reservar um slot com min-height e usar skeleton/placeholder com dimensões equivalentes mantém o layout estável, trocando apenas o conteúdo interno.

Próximo capitúlo

Otimização de componentes e third-parties sem regressões de experiência

Arrow Right Icon
Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.