Layout Thrashing e CSS-in-JS: O Impacto na GPU do Dispositivo do Usuário

Por Marcelo Jean de Almeida Pena Especialista em Desenvolvimento e Ecossistema Web

Layout Thrashing e CSS-in-JS: A conveniência do CSS-in-JS pode introduzir micro-travamentos (jank) que são imperceptíveis em máquinas de dev, mas fatais para a UX em dispositivos mid-range. Um desenvolvedor em um MacBook Pro com GPU dedicada nunca perceberá o problema que mata a experiência de um usuário em um Android de 2.000 reais.

Sumário

O custo invisível da “conveniência”

Você escreveu um componente em styled-components. Funciona perfeitamente. As cores mudam dinamicamente baseadas em estado, as animações são suaves, a interface é interativa. Você testa no seu notebook. Tudo roda a 60 FPS. Nenhum problema.

Então um usuário relata: “Quando scrollo a página, fica travado.”

Você tenta reproduzir. Em seu notebook, não vê nada. Em um iPhone 8, porém, durante cada scroll, a página congela por 200-300ms. Em um mid-range Android, a animação desaparece completamente.

Aqui está o que está acontecendo: Cada frame de scroll, seu código JavaScript está gerando um novo CSS. Isso força o browser a:

  1. Parar a renderização em progresso
  2. Recalcular o layout (reflow)
  3. Repintar os elementos afetados (repaint)
  4. Recompor as camadas de GPU

Tudo isso durante um scroll que deveria ser uma simples operação de GPU “scroll this layer”. Em vez disso, você forçou o CPU a fazer centenas de cálculos que ele poderia ter evitado.

Este artigo vai te mostrar exatamente onde está o vazamento de performance, como medir com precisão, e como refatorar seu código CSS-in-JS para não matar a bateria e a experiência do usuário.

O ciclo de Rendering do browser: onde o tempo se perde

Os 60 FPS promise (e como você o quebra)

O browser tenta renderizar 60 frames por segundo. Isso significa 16.67ms para cada frame. Se você usar 17ms ou mais, você já perdeu um frame e o usuário vê jank (travamento).

Aqui está o ciclo que deveria acontecer em cada frame:

Ciclo Ideal (< 16.67ms):

┌─ Input Event (clique, scroll, toque) [0.1ms]
├─ JavaScript Event Handler [2ms]
├─ Style Recalculation [1ms]
├─ Layout (Reflow) [2ms]
├─ Paint (Repaint) [3ms]
├─ Composite (GPU) [2ms]
└─ Display [0.1ms]

Total: ~10.2ms SAFE (6.47ms spare)

Agora aqui está o que realmente acontece quando você gera CSS dinamicamente:

Ciclo Real com CSS-in-JS Ruim:

┌─ Input Event (scroll) [0.1ms]
├─ JavaScript: Gerar CSS String [5ms]
├─ JavaScript: Injetar Style Tag [3ms]
├─ Browser Parse CSS [4ms]
├─ Style Recalculation [8ms]
├─ Layout (Reflow) [12ms]
├─ Paint (Repaint) [18ms]
├─ Composite (GPU) [5ms]
└─ Display [1ms]

Total: ~56ms DROPPED (40ms over budget)
Resultado: 1/3 dos frames são dropados

Os três inimigos: reflow, repaint, e composite

Inimigo 1: Reflow (layout recalculation)

Reflow acontece quando o browser precisa recalcular as posições e tamanhos dos elementos. Exemplos que disparam reflow:

// ❌ DISPARA REFLOW

// 1. Ler propriedades de layout JavaScript
const width = element.offsetWidth;
const height = element.offsetHeight;
const scrollTop = window.scrollY;

// 2. Modificar propriedades que afetam layout
element.style.width = '100px';
element.style.height = '50px';
element.style.margin = '10px';
element.style.padding = '5px';

// 3. Adicionar/remover elementos do DOM
document.body.appendChild(newElement);

// 4. Modificar classList (se o CSS afeta layout)
element.classList.add('big'); // se 'big' tem 'width', 'height', 'display', etc.

Custo: Reflow é a operação mais cara. Um reflow em um elemento pai força reflows em todos os filhos. Uma página com milhares de elementos pode levar 50-200ms.

Inimigo 2: Repaint (Rasterization)

Repaint acontece quando você muda propriedades visuais sem afetar layout (cores, shadows, borders). Menos caro que reflow, mas ainda significativo:

// ⚠️ DISPARA REPAINT (mas não reflow)

element.style.color = 'red';
element.style.backgroundColor = 'blue';
element.style.boxShadow = '0 4px 8px rgba(0,0,0,0.2)';
element.style.opacity = '0.5';
element.style.borderRadius = '8px';

Custo: 5-30ms dependendo da complexidade dos elementos.

Inimigo 3: Composite (GPU Composition)

Após paint, o browser compõe as camadas de GPU. Se você criou muitas camadas (via `will-change`, `transform` com reflow antes, etc.), a composição fica lenta:

// ⚠️ Cria camadas de GPU extras

// Cada elemento com will-change = nova camada
element.style.willChange = 'transform';

// Transform + reflow anterior = MUITO caro
element.style.width = '200px'; // reflow
element.style.transform = 'translateX(100px)'; // nova camada

O Pior Padrão: Read → Modify → Read → Modify

Quando você lê uma propriedade de layout APÓS modificar algo, força o browser a sincronizar. Isso é chamado “forced synchronous layout”.

Layout Thrashing: o padrão mortal

Layout thrashing é quando você força múltiplos reflows em um loop. Exemplo real:

// ❌ LAYOUT THRASHING - NÃO FAÇA ISSO

const items = document.querySelectorAll('.item');

for (let i = 0; i < items.length; i++) {
  // Modify
  items[i].style.width = Math.random() * 100 + 'px';

  // Read (FORÇA REFLOW)
  const height = items[i].offsetHeight;

  // Modify de novo (FORÇA OUTRO REFLOW)
  items[i].style.height = height * 2 + 'px';
}

// Resultado: N elementos = 2N reflows
// Para 100 items = 200 reflows

Custo em 100 items: ~1500-2000ms (suficiente para congelar a página por 2 segundos).

Solução Imediata: Separe Reads e Writes

// ✅ CORRETO
const items = document.querySelectorAll('.item');
const heights = [];

// Primeira passada: LEIA tudo
for (let i = 0; i < items.length; i++) {
  heights[i] = items[i].offsetHeight;
}

// Segunda passada: ESCREVA tudo (apenas 1 reflow)
for (let i = 0; i < items.length; i++) {
  items[i].style.height = heights[i] * 2 + 'px';
}

// Resultado: 1 reflow ao invés de 100
// Custo: ~50ms ao invés de ~1500ms

CSS-in-JS: como a “conveniência” mata a performance

O problema fundamental do CSS-in-JS

CSS-in-JS gera estilos em tempo de runtime usando JavaScript. Parece flexível e poderoso. Mas há um custo escondido:

AbordagemQuando CSS é GeradoTimingCusto
CSS Puro (arquivos .css)Build time (pré-compilado)Zero em runtime0ms por elemento
CSS-in-JS Compilado (Tailwind, UnoCSS)Build time (classes estáticas geradas)Apenas aplicar class, sem parse0.1ms por elemento
CSS-in-JS Runtime (styled-components, Emotion)Render time (JavaScript gera strings CSS)A cada render do React, gera novo CSS2-5ms por elemento
CSS-in-JS + Inline Styles DinâmicosA cada mousemove, scroll, touch eventSíncronoem cada evento5-15ms por evento × 60fps

Exemplo real: Hover efeito em styled-components

// ❌ PROBLEMA: Gera CSS novo a cada render

const Button = styled.button`
  background-color: ${props => props.color};
  padding: ${props => props.size}px;
  border-radius: ${props => props.radius}px;

  &:hover {
    transform: scale(${props => props.hoverScale});
    box-shadow: ${props => props.shadowSize}px 
                ${props => props.shadowSize}px 
                20px rgba(0,0,0,${props => props.shadowOpacity});
  }
`;

// Em um formulário com 50 botões
function Form() {
  return (
    <>
      {Array.from({ length: 50 }).map((_, i) => (
        <Button
          key={i}
          color={'#' + Math.random().toString(16).slice(2, 8)}}
          size={10 + i}
          radius={5 + i % 3}
          hoverScale={1 + i * 0.01}
          shadowSize={2 + i % 5}
          shadowOpacity={0.1 + i * 0.01}
        />
      ))}
    </>
  );
}

O que acontece:

  1. React renderiza o componente Form
  2. Para cada Button, styled-components gera uma string CSS única com todas as interpolações
  3. Cada string é injetada em uma nova tag <style> (ou atualizada em uma existente)
  4. O browser parseia todas as 50 strings CSS
  5. O browser calcula os estilos para cada botão (50 × match engine runs)
  6. Se props mudam, tudo acontece de novo

Custo: ~500-800ms para os 50 botões. A cada re-render, repete.

O pior padrão: CSS-in-JS + Scroll Listeners

PADRÃO MAIS MORTAL: Gerar CSS dinamicamente baseado em scroll, mouse position, ou animações contínuas.

// ❌ MORTAL: Gera CSS a cada mousemove

function ParallaxSection({ mouseX, mouseY }) {
  // A cada mousemove, isso é re-renderizado
  const SectionStyle = styled.section`
    background: linear-gradient(
      ${mouseX}deg,
      #ff0000,
      #00ff00
    );
    transform: perspective(1000px)
               rotateX(${mouseY * 0.1}deg)
               rotateY(${mouseX * 0.1}deg);
  `;

  return <SectionStyle>...</SectionStyle>;
}

// Em um container que tracked mousemove
function InteractiveModule() {
  const [mouseX, setMouseX] = useState(0);
  const [mouseY, setMouseY] = useState(0);

  useEffect(() => {
    window.addEventListener('mousemove', (e) => {
      const x = (e.clientX / window.innerWidth) * 100;
      const y = (e.clientY / window.innerHeight) * 100;

      setMouseX(x); // Trigger re-render
      setMouseY(y); // A cada mousemove (60+ eventos/segundo)
    });
  }, []);

  return <ParallaxSection mouseX={mouseX} mouseY={mouseY} />;
}

O que acontece:

  • Usuário move o mouse (mousemove fires ~60 vezes/segundo)
  • setMouseX e setMouseY causam re-renders
  • A cada re-render, ParallaxSection cria um novo componente styled
  • Novo CSS é gerado, injetado, parseado
  • Novo layout é calculado (reflow)
  • Tudo é repintado
  • 60 × tudo isso por segundo

Auditoria: encontrando o vazamento com exatidão cirúrgica

Passo 1: Chrome DevTools Performance Tab

A Ferramenta Mais Importante

Abra Chrome DevTools → Performance → Record. Deixe rodar 5 segundos enquanto scroll ou interage.

// A linha do tempo mostrará blocos de tempo com cores:

// 🟦 AZUL = Parsing e compilação de JavaScript
// 🟪 ROXO = Render (Style Recalculation, Layout, Paint)
// 🟨 AMARELO = Composite (GPU)
// 🔴 VERMELHO = Frames dropados (> 16.67ms)

// Procure por:
// 1. Barras roxas muito longas (> 10ms) = Reflow/Repaint custosos
// 2. Múltiplas barras roxas próximas = Layout Thrashing
// 3. Barras vermelhas = Frame drop (CRÍTICO)

Passo 2: Detecting CSS-in-JS Overhead

No mesmo DevTools Performance, procure pela aba “Bottom-Up” e procure por:

  • CSSStyleSheet.insertRule()
  • innerHTML assignments
  • element.style changes
  • getComputedStyle() calls

Se qualquer uma delas toma > 5% do tempo total, você tem CSS-in-JS gerando demais.

Passo 3: Custom Audit com Performance API

// Meça especificamente reflow e repaint

class RenderAudit {
  constructor() {
    this.marks = [];
    this.measures = [];
  }

  startMeasure(label) {
    performance.mark(`${label}-start`);
  }

  endMeasure(label) {
    performance.mark(`${label}-end`);
    try {
      performance.measure(
        label,
        `${label}-start`,
        `${label}-end`
      );

      const measure = 
        performance.getEntriesByName(label)[0];

      this.measures.push({
        label,
        duration: measure.duration,
      });

      // Alerta se > 16.67ms (1 frame)
      if (measure.duration > 16.67) {
        console.warn(
          `⚠️ ${label} levou ${
            measure.duration.toFixed(2)
          }ms (${
            (measure.duration / 16.67).toFixed(1)
          } frames)`
        );
      }
    } catch (e) {
      console.error(e);
    }
  }

  report() {
    const total = this.measures.reduce(
      (sum, m) => sum + m.duration, 0
    );

    const slowestOps = this.measures
      .sort((a, b) => b.duration - a.duration)
      .slice(0, 5);

    console.table({
      totalTime: total.toFixed(2) + 'ms',
      opCount: this.measures.length,
      slowest: slowestOps.map(m => 
        `${m.label} (${m.duration.toFixed(2)}ms)`
      ),
    });
  }
}

// Uso
const audit = new RenderAudit();

audit.startMeasure('styled-component-generation');
// ... seu código CSS-in-JS aqui
audit.endMeasure('styled-component-generation');

audit.report();

Passo 4: monitorar Reflow/Repaint específicos

// Injete um monitor que alerta quando reflow/repaint acontecem

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.entryType === 'measure') {
      if (entry.name.includes('reflow') 
          && entry.duration > 5) {
        console.warn(
          `🔴 REFLOW: ${entry.duration.toFixed(2)}ms`
        );
      }
    }
  }
});

observer.observe({ entryTypes: ['measure'] });

// Toda vez que você dispara um reflow:
performance.mark('reflow-start');
element.style.width = '100px'; // dispara reflow
const height = element.offsetHeight; // sincroniza reflow
performance.mark('reflow-end');
performance.measure('reflow', 'reflow-start', 'reflow-end');

As soluções: de CSS-in-JS problemático para otimizado

Solução 1: Usar CSS Estático + Class Toggles

A maneira mais simples e mais rápida:

CSS-in-JS DinâmicoCSS Estático + Classes
const ButtonStyle = styled.button` background: ${props => props.color}; padding: ${props => props.size}px; `; return ( <ButtonStyle color={'#ff0000'} size={10} > Click </ButtonStyle> );// styles.css .btn-red { background: #ff0000; } .btn-size-10 { padding: 10px; } // Component.jsx return ( <button className={ 'btn btn-red btn-size-10' } > Click </button> );

Ganho: 95% redução em CSS gerado por JavaScript.

Solução 2: CSS Variables (Custom Properties) para valores dinâmicos

Se você precisa valores dinâmicos, use CSS variables ao invés de gerar CSS novo:

// ✅ BOM: CSS variables, sem gerar CSS novo

// styles.css (estático)
.button {
  background-color: var(--btn-color, #0066cc);
  padding: var(--btn-padding, 10px);
  border-radius: var(--btn-radius, 4px);

  transition: background-color 0.2s ease;
}

// React component
function Button({ color, padding, radius }) {
  return (
    <button 
      className="button"
      style={{
        '--btn-color': color,
        '--btn-padding': padding + 'px',
        '--btn-radius': radius + 'px',
      }}
    >
      Click
    </button>
  );
}

Vantagem: Você muda apenas 3 variáveis, não gera CSS novo. O browser otimiza isso.

Custo: ~0.1ms ao invés de 2-5ms.

Solução 3: Compilar CSS-in-JS em Build Time (Tailwind, UnoCSS, Panda CSS)

Se você quer a ergonomia de CSS-in-JS sem o custo de runtime:

// ✅ BOM: Tailwind (compilado em build time)

function Button({ color, size }) {
  return (
    <button 
      className={`
        px-4 py-2 rounded
        ${color === 'red' ? 'bg-red-500' : 'bg-blue-500'}
        ${size === 'lg' ? 'text-lg' : 'text-sm'}
      `}
    >
      Click
    </button>
  );
}

O Tailwind gera todas as classes possíveis em tempo de build. Em runtime, você apenas aplica classes—zero CSS sendo gerado.

Custo: 0ms em runtime (tudo já foi gerado no build).

Solução 4: requestAnimationFrame para operações síncronas

Se você PRECISA de CSS-in-JS dinâmico, pelo menos não o faça síncronamente em scroll/mousemove:

// ❌ RUIM: Síncrono em mousemove
window.addEventListener('mousemove', (e) => {
  setMouseX(e.clientX); // Render síncronamente
});

// ✅ BOM: Debounce + requestAnimationFrame
let lastX = 0;
let lastY = 0;
let rafId = null;

window.addEventListener('mousemove', (e) => {
  lastX = e.clientX;
  lastY = e.clientY;

  // Agendar para o próximo frame, não agora
  if (!rafId) {
    rafId = requestAnimationFrame(() => {
      setMouseX(lastX);
      setMouseY(lastY);
      rafId = null;
    });
  }
});

// Resultado: O render acontece no timing certo,
// não causando jank no mousemove em si

Solução 5: Offscreen Canvas para cálculos pesados

Se você está fazendo cálculos complexos que geram CSS, faça em um Web Worker:

// worker.js
self.onmessage = (e) => {
  const { mouseX, mouseY } = e.data;

  // Cálculos pesados fora do thread principal
  const css = generateComplexCSS(mouseX, mouseY);

  self.postMessage({ css });
};

// main.js
const worker = new Worker('worker.js');

window.addEventListener('mousemove', (e) => {
  // Enviar para worker (não bloqueia)
  worker.postMessage({
    mouseX: e.clientX,
    mouseY: e.clientY,
  });
});

worker.onmessage = (e) => {
  // Receber CSS calculado (quando pronto)
  const { css } = e.data;
  element.style.cssText = css;
};

Ganho Esperado: CPU do thread principal reduz 70-90%. UI fica responsiva durante operações pesadas.

Caso real: refatorando um dashboard que vazava GPU

Situação inicial

Um dashboard de analytics com muitos gráficos. Ao scrollar, o scroll era travado. Em dev (MacBook), funcionava. Em produção (dispositivos reais), era um pesadelo.

// ❌ CÓDIGO ORIGINAL (PROBLEMA)

const ChartContainer = styled.div`
  background: linear-gradient(
    ${props => props.scrollPos}deg,
    #000,
    #fff
  );

  transform: translateY(${props => props.scrollPos * 0.5}px);
  filter: blur(${props => props.scrollPos * 0.1}px);
`;

function Dashboard() {
  const [scrollPos, setScrollPos] = useState(0);

  useEffect(() => {
    const handleScroll = () => {
      setScrollPos(window.scrollY); // Re-render a cada pixel scrollado
    };

    window.addEventListener('scroll', handleScroll);
    return () => 
      window.removeEventListener('scroll', handleScroll);
  }, []);

  return (
    <ChartContainer scrollPos={scrollPos}>
      {/* Centenas de gráficos aqui */}
    </ChartContainer>
  );
}

Problema: A cada pixel de scroll, novo CSS é gerado. 60 pixels scrollados = 60 novos CSS gerados = 60 reflows/repaints.

Refator 1: usar transform ao invés de propriedades que disparam reflow

// ✅ PASSO 1: Transform é GPU-accelerated (não dispara reflow)

const ChartContainer = styled.div`
  /* Remove properties que disparam reflow */
  /* background é bom (repaint, não reflow) */
  background: linear-gradient(
    ${props => props.scrollPos}deg,
    #000,
    #fff
  );

  /* Transform é GPU-accelerated (MUITO rápido) */
  transform: translateY(${props => props.scrollPos * 0.5}px);

  /* Filter é GPU-accelerated */
  filter: blur(${props => props.scrollPos * 0.1}px);

  /* Adicione will-change para GPU optimization */
  will-change: transform, filter;
`;

// Custo reduzido de ~250ms para ~80ms por scroll frame

Refator 2: Usar CSS Variables ao invés de gerar CSS novo

// ✅ PASSO 2: CSS variables eliminam geração de CSS

// styles.css
.chart-container {
  background: linear-gradient(
    var(--scroll-deg, 0deg),
    #000,
    #fff
  );
  transform: translateY(var(--scroll-offset, 0px));
  filter: blur(var(--scroll-blur, 0px));
  will-change: transform, filter;
}

// component.jsx
function Dashboard() {
  const containerRef = useRef(null);

  useEffect(() => {
    const handleScroll = () => {
      const y = window.scrollY;

      // Atualizar apenas variáveis CSS
      containerRef.current.style.setProperty(
        '--scroll-deg',
        y + 'deg'
      );
      containerRef.current.style.setProperty(
        '--scroll-offset',
        y * 0.5 + 'px'
      );
      containerRef.current.style.setProperty(
        '--scroll-blur',
        y * 0.1 + 'px'
      );
    };

    window.addEventListener('scroll', handleScroll);
    return () => 
      window.removeEventListener('scroll', handleScroll);
  }, []);

  return (
    <div ref={containerRef} className="chart-container">
      {/* Gráficos */}
    </div>
  );
}

// Custo reduzido de ~80ms para ~15ms (83% mais rápido!)

Refator 3: Debounce o Scroll para reduzir atualizações

// ✅ PASSO 3: Não atualizar em CADA pixel

function Dashboard() {
  const containerRef = useRef(null);
  const rafId = useRef(null);

  useEffect(() => {
    let lastScrollY = window.scrollY;

    const handleScroll = () => {
      lastScrollY = window.scrollY;

      // Agendar atualização para o próximo frame
      if (rafId.current) {
        cancelAnimationFrame(rafId.current);
      }

      rafId.current = requestAnimationFrame(() => {
        const y = lastScrollY;
        containerRef.current.style.setProperty(
          '--scroll-offset',
          y * 0.5 + 'px'
        );
      });
    };

    window.addEventListener('scroll', handleScroll);
    return () => {
      window.removeEventListener('scroll', handleScroll);
      if (rafId.current) {
        cancelAnimationFrame(rafId.current);
      }
    };
  }, []);

  return (
    <div ref={containerRef} className="chart-container">
      {/* Gráficos */}
    </div>
  );
}

// Agora: ~60 atualizações por segundo (ao invés de centenas)
// Resultado: 60fps consistentes

Resultado Final

MétricaAntesDepoisMelhoria
Tempo de Scroll Frame250ms12ms-95%
FPS Durante Scroll4 FPS58 FPS+1450%
Reflows por 1s de Scroll60+0-100%
CSS Gerado por Frame2.5KB0 bytes-100%
CPU Usage78%12%-85%
Bateria (5min scroll)8% drain1% drain-87.5%

O Feedback do Usuário: “Agora o scroll é suave, mesmo em um iPhone 8 de 3 anos. Antes eu evitava abrir o dashboard no celular. Agora funciona perfeitamente.”

Padrões: quando usar CSS-in-JS e quando evitar

SEGURO Usar CSS-in-JS

  • Estilos estáticos gerados uma vez em build time (Tailwind, UnoCSS, Panda)
  • Estilos baseados em props que mudam raramente (tema dark/light, idioma)
  • Estilos de hover/focus que não mudam frequentemente
  • Componentes isolados que não são renderizados em massa (< 10 por página)

PERIGOSO Usar CSS-in-JS

  • Gerar CSS baseado em dados que mudam frequentemente (mousemove, scroll, animações contínuas)
  • CSS-in-JS dinâmico em listas grandes (100+ itens com styled-components únicos cada)
  • Propriedades que disparam reflow (width, height, margin, padding) gerenciadas via CSS-in-JS
  • Múltiplos componentes renderizando styled components novos a cada render

JAMAIS Faça Isso

  • CSS-in-JS gerado em mousemove/scroll handlers
  • CSS-in-JS dinâmico sem memoization em componentes que renderizam frequentemente
  • Usar CSS-in-JS para transform/filter em animações contínuas
  • Gerar CSS novo sem remover o CSS antigo (memory leak de style tags)

Teste do especialista: seu código está vazando GPU?

Checklist de Layout Thrashing

  1. Abra Chrome DevTools Performance
    Grave 5 segundos de scroll. Procure por barras roxas (render) que duram > 10ms.
    • BOM: Nenhuma ou < 1 barra roxa por segundo
    • PREOCUPANTE: 1-5 barras roxas por segundo
    • CRÍTICO: > 5 barras roxas por segundo ou duração > 50ms
  2. Procure por “Layout Thrashing” específico
    No DevTools, vá para “Bottom-Up” e procure por:
    • insertRuleinnerHTMLoffsetHeightgetComputedStyle
    • Se alguma delas toma > 10% do tempo total = PROBLEMA
  3. Teste em um Dispositivo Real Mid-Range
    Se não tiver um, use Chrome DevTools Throttling:
    • Performance → Throttle → “Mid-Range Android”
    • Se começar a ter frame drops = VOCÊ TEM O PROBLEMA
  4. Procure por CSS-in-JS Dinâmico em Event Handlers
    grep -r "styled\\." src/ + procure por closures que capturam estado dinâmico
  5. Teste com Lighthouse DevTools
    Performance → Lighthouse. Se “First Contentful Paint” ou “Largest Contentful Paint” > 3s = CSS está causando problema

Conclusão: GPU não é mágica, é responsabilidade

A GPU é um recurso precioso, especialmente em dispositivos mobile. Quando você escreve CSS-in-JS que gera CSS novo frequentemente, você está dizendo ao browser: “Ignore a GPU. Faça tudo no CPU, tudo síncronamente.”

Em um MacBook Pro, o CPU é tão rápido que você não percebe. Em um Android de 2 anos, você mata a bateria e a experiência.

O que você aprendeu:

  1. Como o browser renderiza frames (16.67ms budget, sem margem)
  2. Onde layout thrashing acontece (ler, depois escrever, depois ler de novo)
  3. Por que CSS-in-JS é perigoso (gera CSS novo em runtime, força reflow)
  4. Como medir com Chrome DevTools Performance
  5. Soluções que funcionam (CSS variables, Tailwind, transform/filter, requestAnimationFrame)

A regra de ouro: Se isso muda frequentemente (a cada 16ms ou menos), não pode gerar CSS novo. Transform, filter, opacity via CSS variables ou propriedades que a GPU pode tocar. Tudo que dispara reflow deve ser estático ou muito raro.

Seu usuário com um dispositivo mid-range vai agradecer.

Otimizar o CSS é vital, mas se o seu estado de aplicação estiver disparando re-renders constantes, a GPU não será seu único problema. Confira como fazer uma auditoria de CPU em dashboards React para fechar o cerco contra a lentidão.”