Telles Logo
Pular para o conteúdo
Ver todos os artigos
dez 30, 2023
·
views
·
likes
·
6 min

CSS: Critical Path e Layout Thrashing

Como o browser renderiza CSS, o que é o critical rendering path, como evitar layout thrashing e como escrever animações que não travam a tela.

CSS parece inocente comparado ao JavaScript, mas tem seu próprio conjunto de armadilhas de performance. Um arquivo CSS bloqueando a renderização, uma propriedade animada errada ou leituras e escritas DOM alternadas podem custar centenas de milissegundos.

O Pipeline de Renderização

Para entender CSS performance, é preciso entender como o browser renderiza uma página — cada etapa tem seu custo e suas armadilhas.

📄1. Parse HTML + CSS

Constrói o DOM e o CSSOM em paralelo. CSS é render-blocking — o browser não renderiza nada até ter o CSSOM completo.

🌳2. Render Tree

DOM + CSSOM combinados. Apenas elementos visíveis com seus estilos computados. display: none é excluído.

📐3. Layout

Calcula posição e tamanho de cada elemento. Qualquer mudança em propriedades geométricas (width, height, margin) dispara esta fase — caro.

🎨4. Paint

Preenche pixels em layers. Mudanças em background-color, color, border disparam paint — menos caro que layout.

🖥️5. Composite

Combina layers e envia ao display. transform e opacity só chegam aqui — sem layout nem paint. São as propriedades de animação ideais.

🔄Reflow = volta ao início

Qualquer mudança que afeta geometria força o browser a refazer Layout → Paint → Composite para os elementos afetados e seus descendentes.

CSS bloqueia mais do que parece

CSS bloqueia não só a renderização — também bloqueia a execução de JavaScript síncrono. O browser não executa scripts antes de ter o CSSOM, porque o script poderia consultar estilos computados. CSS lento atrasa JS, que atrasa renderização em cascata.

Critical CSS

Critical CSS é o subconjunto mínimo de estilos necessários para renderizar o conteúdo above the fold sem arquivos CSS externos.

<head>
  <!-- ✅ Critical CSS inline — sem download, sem bloqueio -->
  <style>
    body { margin: 0; font-family: Inter, sans-serif; }
    .hero { display: flex; min-height: 100vh; align-items: center; }
    .hero-title { font-size: 3rem; font-weight: 700; }
  </style>
 
  <!-- ✅ CSS não-crítico carregado de forma assíncrona -->
  <link
    rel="stylesheet"
    href="/styles.css"
    media="print"
    onload="this.media='all'"
  />
  <noscript><link rel="stylesheet" href="/styles.css" /></noscript>
</head>

O truque do media="print": o browser baixa o arquivo mas não o trata como render-blocking. Quando carrega, o onload muda media para all, ativando os estilos sem bloqueio.

Layout Thrashing

Layout thrashing acontece quando o browser é forçado a recalcular o layout repetidamente porque o código alterna entre ler e escrever propriedades do DOM.

import { useState, useRef } from "react";

const ITEMS = Array.from({ length: 20 }, (_, i) => `Item ${i + 1}`);

export default function App() {
  const [mode, setMode] = useState("bad");
  const [log, setLog] = useState([]);
  const containerRef = useRef(null);

  function runBad() {
    const start = performance.now();
    const items = containerRef.current.querySelectorAll(".box");
    // ❌ Lê e escreve alternadamente — força o browser a recalcular layout em cada iteração
    items.forEach((el) => {
      const h = el.offsetHeight; // leitura (força layout)
      el.style.height = h + 1 + "px"; // escrita (invalida layout)
    });
    const end = performance.now();
    setLog((prev) => [
      `❌ Layout thrashing: ${(end - start).toFixed(2)}ms`,
      ...prev.slice(0, 4),
    ]);
  }

  function runGood() {
    const start = performance.now();
    const items = containerRef.current.querySelectorAll(".box");
    // ✅ Lê tudo primeiro (batch reads), depois escreve (batch writes)
    const heights = Array.from(items).map((el) => el.offsetHeight);
    items.forEach((el, i) => {
      el.style.height = heights[i] + 1 + "px";
    });
    const end = performance.now();
    setLog((prev) => [
      `✅ Batch read/write: ${(end - start).toFixed(2)}ms`,
      ...prev.slice(0, 4),
    ]);
  }

  return (
    <div style={{ fontFamily: "sans-serif", padding: 16 }}>
      <div style={{ display: "flex", gap: 8, marginBottom: 12 }}>
        <button
          onClick={runBad}
          style={{ padding: "6px 14px", background: "#f38ba8", border: "none", borderRadius: 6, cursor: "pointer" }}
        >
          ❌ Com Thrashing
        </button>
        <button
          onClick={runGood}
          style={{ padding: "6px 14px", background: "#a6e3a1", border: "none", borderRadius: 6, cursor: "pointer" }}
        >
          ✅ Sem Thrashing
        </button>
      </div>
      <div style={{ marginBottom: 12, minHeight: 80 }}>
        {log.map((l, i) => (
          <div key={i} style={{ color: "#cdd6f4", fontSize: 13, marginBottom: 2 }}>{l}</div>
        ))}
      </div>
      <div ref={containerRef} style={{ display: "flex", flexWrap: "wrap", gap: 4 }}>
        {ITEMS.map((item) => (
          <div
            key={item}
            className="box"
            style={{
              padding: "4px 8px",
              background: "#313244",
              borderRadius: 4,
              color: "#cdd6f4",
              fontSize: 12,
            }}
          >
            {item}
          </div>
        ))}
      </div>
    </div>
  );
}

Propriedades que forçam reflow ao serem lidas

  • Propriedades de offset

    element.offsetWidth, offsetHeight, offsetTop, offsetLeft — lidas após uma escrita, forçam reflow imediato

  • getBoundingClientRect()

    Retorna a posição e tamanho do elemento na viewport. Força layout se o DOM estiver "sujo".

  • scrollTop / scrollLeft

    Ler posição de scroll após uma escrita DOM pode forçar layout.

  • getComputedStyle()

    Consulta estilos calculados — requer que o CSSOM esteja atualizado, forçando recálculo se necessário.

A solução: batch reads antes de writes

// ❌ Layout thrashing — lê e escreve alternando por elemento
elements.forEach(el => {
  const h = el.offsetHeight;  // read → força reflow
  el.style.height = h + 1 + 'px'; // write → invalida layout
});
 
// ✅ Batch: lê tudo primeiro, depois escreve tudo
const heights = Array.from(elements).map(el => el.offsetHeight); // batch reads
elements.forEach((el, i) => {
  el.style.height = heights[i] + 1 + 'px'; // batch writes
});

Animações Performáticas

🔴Alto custo — Layout

width, height, margin, padding, top, left — disparam Layout + Paint + Composite. Nunca anime essas propriedades.

🟡Médio custo — Paint

background-color, color, border-color, box-shadow — disparam Paint + Composite. Evite em animações a 60fps.

🟢Baixo custo — Composite

transform e opacity — processados pelo compositor thread, separado do main thread. Rodam a 60fps mesmo com JS bloqueando.

/* ❌ Causa layout em cada frame — jank em dispositivos lentos */
@keyframes slideIn {
  from { left: -100px; }
  to { left: 0; }
}
 
/* ✅ Só compositor — 60fps garantido */
@keyframes slideIn {
  from { transform: translateX(-100px); }
  to { transform: translateX(0); }
}

will-change

/* Promove elemento para compositor layer antecipadamente */
.animated-element {
  will-change: transform, opacity;
}

will-change diz ao browser para criar uma compositor layer antecipadamente, eliminando o custo de criá-la no momento da animação.

Cuidado com will-change

Cada compositor layer consome memória de GPU. Não aplique will-change em todos os elementos — só nos que realmente vão animar. E remova após a animação: element.style.willChange = 'auto'.

Técnicas Avançadas

/* Cartão isolado — mudanças internas não afetam o layout externo */
.card {
  contain: layout style paint;
  /* ou o shorthand: */
  contain: content;
}

contain diz ao browser que um elemento é independente do restante do documento. Atualizar um cartão numa lista não re-triggeriza layout do documento inteiro.

Checklist de CSS Performance

  • CSS crítico inline no HTML

    Estilos necessários para o conteúdo above the fold devem estar inline — zero arquivos externos bloqueando o primeiro paint.

  • Anime só transform e opacity

    Qualquer outra propriedade causa reflow ou repaint, destruindo a fluidez em dispositivos móveis.

  • Evite layout thrashing

    Batch reads antes de writes. Se precisar alternar, use requestAnimationFrame para separar as fases.

  • Use will-change com moderação

    Só em elementos que vão animar, removendo após a animação. Cada layer consome memória de GPU.

  • Considere CSS Containment

    contain: content em componentes isolados (cards, itens de lista) evita reflows globais desnecessários.

  • content-visibility para listas longas

    content-visibility: auto pula renderização de itens fora da viewport sem JavaScript.


No próximo artigo, exploramos o Chrome DevTools em detalhe — Performance panel, flame graph, Memory, Coverage e Lighthouse integrado.

As minhas notas

Selecione um trecho e use Destacar no menu (somente neste aparelho).

Artigos relacionados