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():
- Grave uma interação lenta
- Identifique quais componentes estão lentos
- 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:
- Bundle size aumentou mais de 10KB? Precisa de lazy loading?
- Tem lista com mais de 50 itens? Precisa virtualizar?
- Tem input que dispara requests? Tem debounce?
- Estado está no nível certo da árvore?
- 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.