CLS (Cumulative Layout Shift): Guia Completo para Eliminar Instabilidade Visual no Seu Site

Guia prático sobre CLS (Cumulative Layout Shift), a métrica dos Core Web Vitals que mede a estabilidade visual. Aprenda a diagnosticar e corrigir layout shifts causados por imagens, fontes, anúncios e conteúdo dinâmico com exemplos de código funcionais.

Introdução — O CLS é a Métrica Mais Enganadora dos Core Web Vitals

Depois de termos coberto o INP (responsividade) e o LCP (velocidade de carregamento) nos artigos anteriores desta série, chegou a altura de fechar o ciclo com a terceira — e última — métrica dos Core Web Vitals: o CLS, Cumulative Layout Shift. Se o LCP mede "quando é que o conteúdo principal apareceu" e o INP mede "quando é que o site reagiu ao meu clique", o CLS responde a uma pergunta que todos conhecemos na pele: "o conteúdo ficou quieto ou andou aos saltos?"

Vou ser honesto: o CLS é, dos três, a métrica mais enganadora. Tem a maior percentagem de origens com pontuação "boa" nos dados do CrUX — 80,3% em dezembro de 2025 — e por isso muitas equipas assumem que "isto já está resolvido". Mas há um problema, e não é pequeno.

Ao contrário do LCP e do INP, que são medidos em milissegundos e são relativamente intuitivos, o CLS é um valor adimensional calculado através de frações de impacto e distância. Isto torna-o difícil de diagnosticar, difícil de reproduzir em laboratório e, quando falha, os efeitos na experiência do utilizador são genuinamente irritantes — botões que mudam de posição no instante em que vamos clicar, texto que salta enquanto tentamos ler, compras acidentais porque um anúncio empurrou um botão. Já nos aconteceu a todos, certo?

Os limiares definidos pelo Google são:

  • Bom: ≤ 0,1
  • Precisa de Melhorias: 0,1 – 0,25
  • Fraco: > 0,25

O Google exige que pelo menos 75% das visitas de uma origem tenham CLS "bom" para que a métrica passe nos Core Web Vitals. Sites que falham perdem sinal de ranking — e com as AI Overviews a dominar cada vez mais os resultados de pesquisa, a margem para sites com má experiência está a ficar cada vez mais apertada.

Neste guia, vamos dissecar o CLS ao nível técnico, identificar as causas mais comuns (com dados reais) e implementar correções com código que funciona de verdade. Se já leu os nossos guias de INP e LCP, este artigo completa a trilogia. Se não leu, não se preocupe — este guia é totalmente autossuficiente.

Como o CLS é Calculado — Session Windows e a Fórmula Real

Antes de otimizar o que quer que seja, é essencial entender exatamente o que o browser mede. E é aqui que muita gente se engana.

O CLS não é simplesmente a soma de todos os layout shifts da página. Em 2021, o Chrome alterou a definição para utilizar session windows — janelas de sessão que agrupam layout shifts consecutivos com menos de 1 segundo de intervalo entre si e uma duração máxima de 5 segundos. O score final de CLS é o valor da maior session window, não a soma de todas. Esta distinção é importante e surpreende muita gente.

Cada layout shift individual é calculado com esta fórmula:

Layout Shift Score = Fração de Impacto × Fração de Distância

  • Fração de Impacto: a percentagem do viewport afetada pelo movimento (área que o elemento ocupava antes + área que ocupa depois, unidas)
  • Fração de Distância: a maior distância que qualquer elemento instável percorreu, dividida pela maior dimensão do viewport (largura ou altura)

Isto tem uma implicação prática que vale a pena sublinhar: um único elemento grande que se desloca ligeiramente pode ter um score pior do que vários elementos pequenos que saltam bastante. Por exemplo, uma imagem hero de largura total que se desloca 100px verticalmente afeta uma fração enorme do viewport e gera um score elevado — mesmo que visualmente o deslocamento pareça "pequeno".

Outra subtileza crítica: o CLS só conta shifts que não são precedidos por interação do utilizador. Se alguém clica num botão "Ver Mais" e o conteúdo expande, isso é esperado e não conta. Mas se o conteúdo se desloca enquanto o utilizador está simplesmente a ler — isso sim, é um problema.

As 5 Principais Causas de CLS (Com Dados Reais)

1. Imagens e Vídeos Sem Dimensões

Esta é, de longe, a causa mais frequente de CLS na web. Quando uma tag <img> ou <video> não tem atributos width e height, o browser aloca inicialmente zero pixels de espaço vertical. Quando o recurso carrega e o browser descobre o tamanho real, todo o conteúdo abaixo é empurrado — e em imagens hero, isto pode facilmente atingir scores de 0,3 ou mais.

O que piora a situação é que muitos developers pensam que já resolveram o problema ao usar CSS width: 100%; height: auto;. Mas isto não é suficiente sem os atributos HTML — o browser precisa dos valores width e height no HTML para calcular o aspect ratio antes de o CSS ser processado. É um detalhe subtil mas que faz toda a diferença.

2. Web Fonts e o Problema do FOUT/FOIT

As fontes web são o que eu costumo chamar o "assassino silencioso" do CLS. Mais de 80% dos websites utilizam web fonts, e o problema manifesta-se de duas formas:

  • FOIT (Flash of Invisible Text): o texto fica invisível enquanto a fonte carrega, depois aparece de repente — causando um recálculo de layout
  • FOUT (Flash of Unstyled Text): o texto aparece com a fonte de fallback (por exemplo, Arial) e depois é substituído pela fonte personalizada — que tem métricas diferentes, fazendo o texto ocupar mais ou menos espaço

E aqui está o paradoxo que pouca gente conhece: font-display: swap, frequentemente recomendado para melhorar o LCP (ao mostrar texto rapidamente com o fallback), pode piorar o CLS se as métricas da fonte de fallback forem diferentes das da fonte web. Um trade-off que apanha muitas equipas desprevenidas.

3. Anúncios, Iframes e Embeds Dinâmicos

Se o seu site depende de receitas publicitárias, provavelmente já sentiu esta dor. Os anúncios carregam de forma assíncrona, frequentemente com tamanhos variáveis, e quando são inseridos no DOM empurram todo o conteúdo editorial para baixo. Dados indicam que sites com muitos anúncios têm uma média de CLS de ~0,3 — três vezes acima do limiar "bom".

O problema piora quando o anúncio simplesmente não aparece (o slot fica vazio e depois colapsa) ou quando aceita múltiplos tamanhos — 300×250 num momento, 300×600 no seguinte. Ambas as situações geram shifts que irritam o utilizador.

4. Conteúdo Injetado Dinamicamente via JavaScript

Banners de cookies, barras de notificação, widgets de chat, conteúdo carregado via AJAX que é inserido acima do conteúdo existente — tudo isto causa CLS. A regra é simples: qualquer conteúdo inserido no DOM que empurre elementos já visíveis conta como layout shift. Sem exceções.

Em aplicações React/Vue/Angular, os skeleton screens que não correspondem à altura final do conteúdo são outra fonte comum (e muitas vezes ignorada). Se o skeleton tem 200px de altura e o conteúdo real tem 350px, há um shift de 150px quando o conteúdo substitui o placeholder. Convém acertar nessas medidas.

5. Animações Não-Compostas

Animações CSS que utilizam propriedades como top, left, width ou height provocam recálculos de layout em cada frame, gerando shifts contínuos. Isto é particularmente problemático em carrosséis com reprodução automática — que podem gerar shifts praticamente infinitos ao longo do ciclo de vida da página.

Estratégias de Correção — Com Código Funcional

4.1 Dimensões Explícitas em Todas as Imagens e Vídeos

A correção mais simples e com maior impacto. Adicione sempre width e height às tags <img> e <video>:

<!-- Mau — provoca CLS -->
<img src="/hero.webp" alt="Imagem principal">

<!-- Bom — previne CLS -->
<img
  src="/hero.webp"
  alt="Imagem principal"
  width="1200"
  height="630"
>

Os browsers modernos utilizam automaticamente estes atributos para calcular o aspect-ratio na user-agent stylesheet, reservando o espaço correto antes de a imagem carregar. O CSS responsivo continua a funcionar normalmente:

img {
  max-width: 100%;
  height: auto;
}

Para containers de vídeo e iframes, utilize a propriedade CSS aspect-ratio diretamente:

.video-container {
  width: 100%;
  aspect-ratio: 16 / 9;
}

.video-container iframe {
  width: 100%;
  height: 100%;
}

Esta propriedade substitui o antigo hack do padding-top: 56.25% com posicionamento absoluto — muito mais limpo e sem markup extra.

4.2 Resolver o CLS de Web Fonts com Fallbacks Metricamente Ajustados

Esta é uma técnica que muitos artigos sobre CLS ignoram completamente, mas que na prática é extremamente eficaz. Em vez de simplesmente usar font-display: swap e esperar pelo melhor, podemos ajustar as métricas da fonte de fallback para corresponder o mais possível à fonte web:

<!-- 1. Preload da fonte crítica -->
<link
  rel="preload"
  href="/fonts/inter-latin.woff2"
  as="font"
  type="font/woff2"
  crossorigin
>

<style>
  /* 2. Fallback com métricas ajustadas */
  @font-face {
    font-family: "Inter Fallback";
    src: local("Arial");
    ascent-override: 90%;
    descent-override: 22%;
    line-gap-override: 0%;
    size-adjust: 107%;
  }

  /* 3. Fonte web com font-display: optional */
  @font-face {
    font-family: "Inter";
    src: url("/fonts/inter-latin.woff2") format("woff2");
    font-display: optional;
    font-weight: 100 900;
  }

  /* 4. Font stack com fallback metricamente alinhado */
  body {
    font-family: "Inter", "Inter Fallback", system-ui, sans-serif;
  }
</style>

As propriedades ascent-override, descent-override, line-gap-override e size-adjust permitem que a fonte de fallback ocupe exatamente o mesmo espaço que a fonte web. Quando a troca acontece (se acontecer — com font-display: optional, pode nem sequer ocorrer), o layout fica quieto.

Aliás, o font-display: optional é provavelmente a melhor opção se o CLS zero for a prioridade: dá ao browser uma janela de ~100ms para usar a fonte personalizada. Se não estiver pronta, usa o fallback para toda a sessão e armazena a fonte em cache para a próxima navegação. Sem troca, sem shift. Simples.

Ferramentas úteis: o Fallback Font Generator ajuda a calcular os valores de override, e frameworks como @next/font e @nuxtjs/fontaine automatizam o processo.

Nota importante: em fevereiro de 2026, o Safari ainda não suporta ascent-override e descent-override. Testem sempre em múltiplos browsers. O font-display: optional, contudo, tem suporte universal.

4.3 Reservar Espaço para Anúncios e Conteúdo Dinâmico

A regra de ouro para anúncios é uma só: reserve sempre o espaço máximo que o anúncio pode ocupar, e nunca colapse o container quando o anúncio não aparece.

<!-- Container para anúncio com espaço reservado -->
<div class="ad-slot" style="min-height: 250px;">
  <!-- O anúncio será injetado aqui -->
</div>

<style>
  .ad-slot {
    min-height: 250px; /* Altura mínima do formato de anúncio */
    width: 100%;
    background-color: #f5f5f5; /* Placeholder visual */
    display: flex;
    align-items: center;
    justify-content: center;
  }

  .ad-slot:empty::after {
    content: "Publicidade";
    color: #999;
    font-size: 0.875rem;
  }
</style>

Para banners de cookies e barras de notificação, a melhor abordagem é usar posicionamento fixo (position: fixed) em vez de inserir o elemento no fluxo do documento:

.cookie-banner {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 1000;
  /* Não afeta o layout — não gera CLS */
}

Conteúdo inserido com position: fixed ou position: absolute não participa no fluxo normal do documento e, portanto, não conta para o CLS. Uma solução elegante para um problema comum.

4.4 Usar CSS transform em Vez de Propriedades de Layout

As animações são uma fonte de CLS que muitos developers ignoram — até serem apanhados por ela. A diferença entre animar com propriedades de layout e com transform é brutal:

/* Mau — provoca recálculos de layout (CLS) */
.elemento-animado {
  transition: top 0.3s, left 0.3s;
}
.elemento-animado:hover {
  top: 10px;
  left: 10px;
}

/* Bom — composited, sem layout shift */
.elemento-animado {
  transition: transform 0.3s;
}
.elemento-animado:hover {
  transform: translate(10px, 10px);
}

As propriedades transform e opacity são processadas na GPU através de composição — não provocam recálculos de layout nem de pintura. Isto elimina shifts e, como bónus, torna as animações muito mais suaves.

Para carrosséis, usem transform: translateX() em vez de alterar left ou margin-left:

.carousel-track {
  display: flex;
  transition: transform 0.5s ease;
}

/* Mover para o 2º slide */
.carousel-track.slide-2 {
  transform: translateX(-100%);
}

4.5 content-visibility para Páginas Longas (Com Cuidado)

A propriedade CSS content-visibility: auto pode melhorar drasticamente o desempenho de renderização ao saltar o rendering de elementos fora do viewport. Mas tem uma armadilha — e é das que doem: sem contain-intrinsic-size, provoca CLS porque o browser trata os elementos ocultos como tendo altura zero.

/* Mau — provoca CLS quando o utilizador faz scroll */
.secao-longa {
  content-visibility: auto;
}

/* Bom — reserva espaço estimado */
.secao-longa {
  content-visibility: auto;
  contain-intrinsic-size: auto 800px;
}

O valor auto 800px indica ao browser para usar 800px como estimativa inicial, mas substituir pelo tamanho real se o elemento já tiver sido renderizado anteriormente. Se a estimativa não for exata, haverá um ligeiro shift — mas é incomparavelmente menor do que sem contain-intrinsic-size.

4.6 Garantir Elegibilidade para bfcache

O back/forward cache (bfcache) é, na minha opinião, uma das formas mais eficazes de evitar CLS — e o melhor de tudo: é completamente gratuita. Quando uma página é restaurada do bfcache, é apresentada instantaneamente no estado exato em que foi deixada. Sem carregamento, sem layout shifts, sem stress.

Para garantir que as vossas páginas são elegíveis para bfcache:

  • Não usem o evento unload — substituam por pagehide
  • Evitem Cache-Control: no-store no documento HTML
  • Fechem ligações WebSocket e IndexedDB antes da navegação
  • Não usem window.opener (abram links externos com rel="noopener")

No Chrome DevTools, podem verificar a elegibilidade em Application → Back/forward cache e testar com o botão "Test back/forward cache". Vale a pena verificar — muitos sites perdem esta otimização gratuita por causa de um unload esquecido algures no código.

Como Diagnosticar e Medir o CLS na Prática

5.1 Com a API PerformanceObserver (Dados de Campo)

A forma mais fiável de medir o CLS é com dados reais de utilizadores. Ponto final. A Layout Instability API permite capturar layout shifts programaticamente:

// Monitorizar CLS em tempo real com atribuição de elementos
let clsValue = 0;
let clsEntries = [];

const observer = new PerformanceObserver((entryList) => {
  for (const entry of entryList.getEntries()) {
    // Ignorar shifts causados por interação do utilizador
    if (entry.hadRecentInput) continue;

    clsValue += entry.value;
    clsEntries.push(entry);

    // Identificar os elementos responsáveis
    if (entry.sources && entry.sources.length > 0) {
      const culprit = entry.sources[0];
      console.log('Layout shift detectado:', {
        score: entry.value.toFixed(4),
        clsAcumulado: clsValue.toFixed(4),
        elemento: culprit.node,
        posicaoAnterior: culprit.previousRect,
        posicaoAtual: culprit.currentRect
      });
    }
  }
});

observer.observe({ type: 'layout-shift', buffered: true });

A opção buffered: true é essencial — garante que o observer captura shifts que ocorreram antes da sua inicialização. O array sources reporta até 5 elementos DOM que mais contribuíram para cada shift, ordenados por área de impacto.

Para enviar dados para o vosso sistema de analytics, usem a biblioteca web-vitals com o build de atribuição:

import { onCLS } from 'web-vitals/attribution';

onCLS((metric) => {
  console.log('CLS final:', metric.value);
  console.log('Maior session window:', metric.attribution);
  console.log('Elemento principal:', metric.attribution.largestShiftTarget);
  console.log('Tempo do shift:', metric.attribution.largestShiftTime);

  // Enviar para analytics
  navigator.sendBeacon('/analytics', JSON.stringify({
    name: metric.name,
    value: metric.value,
    id: metric.id,
    attribution: metric.attribution
  }));
});

5.2 Com o Chrome DevTools

Para diagnóstico em laboratório, o Chrome DevTools oferece várias ferramentas que valem a pena conhecer:

  1. Performance panel: gravem uma sessão, procurem a track "Layout Shifts" com barras roxas, e cliquem nos diamantes para ver animações dos shifts
  2. Layout Shift Regions: ativem em Settings → More Tools → Rendering → Layout Shift Regions — as áreas que sofrem shifts são destacadas a azul em tempo real (muito útil para debug visual)
  3. Live Metrics: o painel de métricas ao vivo no Performance panel mostra o score CLS em tempo real enquanto interagem com a página

Dica profissional: ao testar CLS no DevTools, limpem a cache e simulem uma ligação lenta (3G). Muitos layout shifts só se manifestam quando os recursos demoram a carregar — condição que é completamente invisível na ligação rápida que usamos no desenvolvimento.

5.3 PageSpeed Insights e CrUX

O PageSpeed Insights é a ferramenta mais direta para verificar o CLS do vosso site com dados reais. Apresenta tanto dados de laboratório (Lighthouse) como dados de campo (CrUX — Chrome User Experience Report, baseado em 28 dias de dados de utilizadores reais do Chrome).

O Google Search Console também sinaliza URLs com CLS problemático na secção Core Web Vitals, o que permite identificar padrões. Por exemplo, todas as páginas de produto com o mesmo template podem estar a sofrer do mesmo problema de anúncios sem espaço reservado. Quando identificam uma destas situações, a correção no template resolve dezenas (ou centenas) de páginas de uma só vez.

Checklist de Auditoria CLS — 12 Pontos Essenciais

Usem esta checklist para auditar sistematicamente o CLS do vosso site. Na minha experiência, percorrer estes pontos um a um costuma revelar pelo menos dois ou três problemas que passaram despercebidos:

  1. Todas as tags <img> têm atributos width e height?
  2. Todos os iframes e embeds têm espaço reservado com aspect-ratio ou dimensões fixas?
  3. As fontes web usam font-display: optional ou têm fallbacks metricamente ajustados?
  4. As fontes críticas estão a ser pré-carregadas com <link rel="preload">?
  5. Os slots de anúncios têm min-height definido e não colapsam quando vazios?
  6. Banners de cookies e notificações usam position: fixed?
  7. Nenhum conteúdo é injetado acima do conteúdo existente sem interação do utilizador?
  8. Os skeleton screens correspondem à altura final do conteúdo?
  9. Todas as animações usam transform/opacity em vez de propriedades de layout?
  10. Elementos com content-visibility: auto têm contain-intrinsic-size?
  11. A página é elegível para bfcache (sem unload, sem no-store)?
  12. O CLS de campo está a ser monitorizado via web-vitals ou PerformanceObserver?

Perguntas Frequentes (FAQ)

O que é um bom score de CLS?

Um bom score de CLS é 0,1 ou menos. Valores entre 0,1 e 0,25 necessitam de melhorias, e acima de 0,25 é considerado fraco. O Google exige que pelo menos 75% das visitas ao vosso site tenham CLS "bom" para que a métrica passe nos Core Web Vitals e contribua positivamente para o ranking.

Porque é que o meu CLS é diferente no Lighthouse e no CrUX?

Boa pergunta, e é das mais comuns. O Lighthouse mede CLS em condições de laboratório — num ambiente controlado, durante o carregamento inicial da página. O CrUX mede dados reais de utilizadores ao longo de 28 dias, incluindo toda a duração da sessão (scroll, interações, navegação). É perfeitamente normal ter CLS zero no Lighthouse mas CLS elevado no CrUX, porque muitos layout shifts acontecem depois do carregamento inicial — por exemplo, quando anúncios carregam tardiamente ou quando o utilizador faz scroll e encontra imagens lazy-loaded sem dimensões.

O lazy loading de imagens pode causar CLS?

Sim, pode. Se as imagens carregadas com loading="lazy" não tiverem atributos width e height definidos, quando o utilizador faz scroll e a imagem começa a carregar, o browser não sabe as dimensões e ocorre um layout shift. A solução é simples: adicionem sempre width e height a todas as imagens, independentemente de usarem lazy loading ou não.

Como é que as web fonts causam CLS e como posso resolver?

As web fonts causam CLS quando a fonte de fallback (por exemplo, Arial) tem métricas diferentes da fonte web (por exemplo, Inter). Quando a troca ocorre — especialmente com font-display: swap — o texto pode ocupar mais ou menos espaço, deslocando todo o layout. A melhor solução é usar font-display: optional combinado com propriedades CSS como size-adjust, ascent-override e descent-override para alinhar metricamente a fonte de fallback com a fonte web.

Os anúncios estão a destruir o meu CLS — o que posso fazer?

Reservem sempre espaço com min-height no container do anúncio, dimensionado para o maior formato que pode ser servido. Nunca colapsem o container quando o anúncio não aparece — usem um placeholder visual. Para anúncios que aceitam múltiplos tamanhos, considerem fixar o tamanho do slot ou usar aspect-ratio para manter a estabilidade. O Publisher Ads Audits for Lighthouse do Google é uma ferramenta que vale a pena experimentar para identificar shifts específicos causados por anúncios.

Sobre o Autor Editorial Team

Our team of expert writers and editors.