6 min

Performance no React: Além do memo()

Estratégias avançadas de otimização que vão além dos hooks básicos e realmente fazem diferença.

  • React
  • Performance

Todo tutorial de performance em React começa com React.memo(), useMemo() e useCallback(). Esses hooks têm seu lugar, mas a maioria dos problemas de performance que encontro na prática vem de lugares diferentes.

O problema não é re-render

A obsessão com “evitar re-renders” está mal direcionada. React é muito bom em re-renderizar. O Virtual DOM existe exatamente para isso.

O problema real geralmente é:

  • Componentes fazendo trabalho demais durante o render
  • Layout thrashing no DOM
  • Requests desnecessários ou mal gerenciados
  • Bundle muito grande

Meça antes de otimizar

React DevTools Profiler é seu amigo. Antes de adicionar qualquer memo():

  1. Grave uma interação lenta
  2. Identifique quais componentes estão lentos
  3. Entenda por que estão lentos

Muitas vezes o problema não é o componente que você imagina.

// Você acha que isso é o problema
const ExpensiveList = memo(({ items }) => {
  return items.map(item => <ListItem key={item.id} item={item} />);
});

// Mas o problema real é isso
function ListItem({ item }) {
  // Cálculo pesado que roda em todo render
  const processedData = heavyComputation(item.data);

  return <div>{processedData}</div>;
}

Estratégias que realmente funcionam

1. Mover estado para baixo

Quanto mais alto o estado na árvore, mais componentes re-renderizam quando ele muda.

// Ruim: estado no topo afeta toda a árvore
function Page() {
  const [isHovered, setIsHovered] = useState(false);

  return (
    <Layout>
      <Header />
      <Content />
      <Button
        onMouseEnter={() => setIsHovered(true)}
        onMouseLeave={() => setIsHovered(false)}
        isHovered={isHovered}
      />
    </Layout>
  );
}

// Bom: estado encapsulado onde é usado
function HoverableButton() {
  const [isHovered, setIsHovered] = useState(false);

  return (
    <Button
      onMouseEnter={() => setIsHovered(true)}
      onMouseLeave={() => setIsHovered(false)}
      isHovered={isHovered}
    />
  );
}

2. Composition over props

Passar children como prop é mais eficiente que passar dados.

// Menos eficiente: Header re-renderiza quando count muda
function Page() {
  const [count, setCount] = useState(0);

  return (
    <Layout header={<Header />}>
      <Counter count={count} setCount={setCount} />
    </Layout>
  );
}

// Mais eficiente: Header é children, não recria em cada render
function Page() {
  return (
    <Layout>
      <Header />
      <CounterSection />
    </Layout>
  );
}

function CounterSection() {
  const [count, setCount] = useState(0);
  return <Counter count={count} setCount={setCount} />;
}

3. Virtualização para listas longas

Se você tem mais de ~100 itens, virtualize. Não tem discussão.

import { useVirtualizer } from '@tanstack/react-virtual';

function VirtualList({ items }) {
  const parentRef = useRef(null);

  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 50,
  });

  return (
    <div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}>
      <div style={{ height: virtualizer.getTotalSize() }}>
        {virtualizer.getVirtualItems().map((virtualItem) => (
          <div
            key={virtualItem.key}
            style={{
              position: 'absolute',
              top: virtualItem.start,
              height: virtualItem.size,
            }}
          >
            {items[virtualItem.index].name}
          </div>
        ))}
      </div>
    </div>
  );
}

4. Debounce em inputs

Input controlado + API call = disaster. Sempre debounce.

function SearchInput() {
  const [value, setValue] = useState('');
  const [results, setResults] = useState([]);

  // Debounce a busca, não o estado do input
  const debouncedSearch = useMemo(
    () => debounce(async (query: string) => {
      const data = await searchAPI(query);
      setResults(data);
    }, 300),
    []
  );

  return (
    <input
      value={value}
      onChange={(e) => {
        setValue(e.target.value);
        debouncedSearch(e.target.value);
      }}
    />
  );
}

5. Code splitting inteligente

Lazy load rotas é básico. O próximo nível é lazy load componentes pesados dentro de rotas.

// Lazy load de rota (básico)
const Dashboard = lazy(() => import('./pages/Dashboard'));

// Lazy load de componente pesado dentro da página
function Dashboard() {
  const [showChart, setShowChart] = useState(false);

  const HeavyChart = lazy(() => import('./components/HeavyChart'));

  return (
    <div>
      <Stats />
      <button onClick={() => setShowChart(true)}>Ver gráfico</button>
      {showChart && (
        <Suspense fallback={<ChartSkeleton />}>
          <HeavyChart />
        </Suspense>
      )}
    </div>
  );
}

O checklist que uso

Antes de mergear qualquer PR com componentes novos:

  1. Bundle size aumentou mais de 10KB? Precisa de lazy loading?
  2. Tem lista com mais de 50 itens? Precisa virtualizar?
  3. Tem input que dispara requests? Tem debounce?
  4. Estado está no nível certo da árvore?
  5. Profiler mostra algum componente com > 16ms de render?

Performance não é sobre micro-otimizações. É sobre não fazer trabalho desnecessário em primeiro lugar.