Por Que Seu App React Re-renderiza Demais: Um Mergulho Profundo em Otimização de Performance
Por Que Seu App React Re-renderiza Demais: Um Mergulho Profundo em Otimização de Performance
Você construiu uma aplicação React linda. O código está limpo, os componentes bem estruturados, e tudo funciona. Mas algo está errado. Digitar num campo de formulário parece lento. Scroll numa lista trava. Abrir um modal leva um momento perceptível. Seu app parece... lento.
Você abre o React DevTools Profiler, e seu coração afunda. Componentes estão re-renderizando 47 vezes quando você digita um único caractere. Um simples clique de botão cascateia em mais de 200 atualizações de componentes. A árvore inteira do app acende como uma árvore de Natal a cada mudança de estado.
Você tem um problema de re-renderização. E você não está sozinho.
Este é o problema de performance mais comum em aplicações React, e também o mais mal-entendido. Desenvolvedores recorrem a React.memo, useMemo, e useCallback como encantamentos mágicos, espalhando-os por todo lugar esperando que algo funcione. Spoiler: essa abordagem geralmente piora as coisas.
Neste mergulho profundo, vamos dissecar exatamente por que componentes React re-renderizam, identificar os padrões que causam mais dano, e percorrer otimizações do mundo real que reduziram contagens de render em 80% em aplicações de produção. Sem programação cargo cult—apenas entendendo o sistema e aplicando correções direcionadas.
O Modelo Mental de Re-renderização do React
Antes de otimizar, você precisa entender o que dispara uma re-renderização. O comportamento de re-renderização do React segue regras simples:
Regra 1: Mudanças de Estado Disparam Re-renderizações
Quando o estado de um componente muda via useState ou useReducer, aquele componente re-renderiza:
function Counter() { const [count, setCount] = useState(0); // Cada clique dispara uma re-renderização do Counter return <button onClick={() => setCount(c => c + 1)}>{count}</button>; }
Isso é esperado e necessário. Nenhuma otimização é necessária aqui.
Regra 2: Re-renderizações do Pai Se Propagam para os Filhos
Quando um componente re-renderiza, todos os seus filhos também re-renderizam, independentemente de suas props terem mudado:
function Parent() { const [count, setCount] = useState(0); return ( <div> <button onClick={() => setCount(c => c + 1)}>Count: {count}</button> {/* ExpensiveChild re-renderiza em CADA mudança de count */} {/* mesmo não recebendo props relacionadas ao count */} <ExpensiveChild /> </div> ); } function ExpensiveChild() { // Isso roda em cada re-renderização do pai console.log('ExpensiveChild rendered'); return <div>Eu sou caro para renderizar</div>; }
Esta é a fonte de 90% dos problemas de performance. O pai tem estado, então ele re-renderiza. O filho não liga para aquele estado, mas re-renderiza mesmo assim porque o React não sabe disso.
Regra 3: Mudanças de Context Re-renderizam Todos os Consumers
Todo componente que chama useContext(SomeContext) vai re-renderizar quando o valor daquele context mudar:
const ThemeContext = createContext({ theme: 'light' }); function App() { const [theme, setTheme] = useState('light'); const [user, setUser] = useState(null); // Problema: mudar user faz consumers de theme re-renderizarem // porque o objeto value inteiro é recriado return ( <ThemeContext.Provider value={{ theme, user, setUser }}> <ThemedButton /> {/* Re-renderiza quando user muda! */} </ThemeContext.Provider> ); }
Esta é a segunda maior fonte de problemas de performance—valores de context que mudam muito frequentemente ou contêm coisas demais.
Identificando o Problema: React DevTools Profiler
Antes de otimizar qualquer coisa, você precisa de dados. Abra o React DevTools e navegue até a aba Profiler.
Passo 1: Grave uma Interação Problemática
Clique em "Start profiling" e realize a ação que parece lenta. Digite num input, faça scroll numa lista, ou abra/feche um modal. Depois pare o profiling.
Passo 2: Analise o Flamegraph
O flamegraph te mostra:
- Quais componentes renderizaram (barras coloridas)
- Quanto tempo cada render levou (largura da barra)
- Por que eles renderizaram (passe o mouse para detalhes)
Procure por:
- Componentes renderizando quando não deveriam (barras cinzas que deveriam ser amarelas)
- O mesmo componente renderizando múltiplas vezes (barras repetidas na timeline)
- Componentes caros renderizando frequentemente (barras largas aparecendo frequentemente)
Passo 3: Habilite "Highlight updates when components render"
Nas configurações do React DevTools, habilite essa opção. Agora interaja com seu app. Componentes que re-renderizarem vão piscar. Se seu app inteiro pisca quando você digita um caractere, você encontrou seu problema.
Os Maiores Erros de Re-renderização (E Como Corrigi-los)
Erro 1: Criar Objetos/Arrays no Render
Este é o erro mais comum. Criar novos objetos ou arrays durante o render faz componentes filhos receberem props "novas" toda vez:
// ❌ RUIM: Cria novo array em cada render function TodoList({ todos }) { return ( <List items={todos.filter(t => !t.completed)} // Novo array toda vez config={{ showDates: true }} // Novo objeto toda vez /> ); } // ✅ BOM: Referências estáveis function TodoList({ todos }) { const activeTodos = useMemo( () => todos.filter(t => !t.completed), [todos] ); const config = useMemo( () => ({ showDates: true }), [] // Deps vazias = nunca muda ); return <List items={activeTodos} config={config} />; }
Melhor ainda—se o config nunca muda, mova para fora do componente:
// Melhor: Completamente fora do ciclo de render const LIST_CONFIG = { showDates: true }; function TodoList({ todos }) { const activeTodos = useMemo( () => todos.filter(t => !t.completed), [todos] ); return <List items={activeTodos} config={LIST_CONFIG} />; }
Erro 2: Props de Funções Inline
Passar funções inline como props cria novas referências de função em cada render:
// ❌ RUIM: Nova referência de função cada render function TodoItem({ todo, onToggle }) { return ( <Checkbox checked={todo.completed} onChange={() => onToggle(todo.id)} // Nova função toda vez /> ); } // ✅ BOM: Callback estável function TodoItem({ todo, onToggle }) { const handleToggle = useCallback( () => onToggle(todo.id), [todo.id, onToggle] ); return ( <Checkbox checked={todo.completed} onChange={handleToggle} /> ); }
Importante: useCallback só ajuda se o componente filho está memoizado (React.memo) ou usa o callback no seu próprio array de dependências. Caso contrário, você está adicionando overhead sem benefício.
Erro 3: Elevar o Estado Alto Demais
O estado deve viver o mais perto possível de onde é usado:
// ❌ RUIM: Estado do formulário no App causa re-renderização de toda a árvore function App() { const [formData, setFormData] = useState({ name: '', email: '' }); return ( <div> <Header /> {/* Re-renderiza a cada tecla */} <Sidebar /> {/* Re-renderiza a cada tecla */} <Form formData={formData} setFormData={setFormData} /> <Footer /> {/* Re-renderiza a cada tecla */} </div> ); } // ✅ BOM: Estado colocado junto com seu uso function App() { return ( <div> <Header /> <Sidebar /> <Form /> {/* O estado vive aqui */} <Footer /> </div> ); } function Form() { const [formData, setFormData] = useState({ name: '', email: '' }); // Só Form e seus filhos re-renderizam ao digitar return (/* ... */); }
Erro 4: Recriar o Objeto Value do Context
Valores de context são comparados por referência. Se você cria um novo objeto em cada render, cada consumer re-renderiza:
// ❌ RUIM: Novo objeto cada render = todos os consumers re-renderizam function AuthProvider({ children }) { const [user, setUser] = useState(null); return ( <AuthContext.Provider value={{ user, setUser, isLoggedIn: !!user }}> {children} </AuthContext.Provider> ); } // ✅ BOM: Valor memoizado function AuthProvider({ children }) { const [user, setUser] = useState(null); const value = useMemo( () => ({ user, setUser, isLoggedIn: !!user }), [user] // Só recria quando user muda ); return ( <AuthContext.Provider value={value}> {children} </AuthContext.Provider> ); }
Erro 5: Um Único Mega-Context
Colocar tudo num context significa que cada mudança re-renderiza cada consumer:
// ❌ RUIM: Um context para tudo const AppContext = createContext({ user: null, theme: 'light', notifications: [], sidebarOpen: false, // ... mais 20 propriedades }); // Todo componente usando useContext(AppContext) re-renderiza // quando QUALQUER um desses valores muda // ✅ BOM: Dividir contexts por frequência de atualização const UserContext = createContext(null); // Raramente muda const ThemeContext = createContext('light'); // Quase nunca muda const NotificationContext = createContext([]); // Muda frequentemente const UIContext = createContext({}); // Muda na interação
Padrões Avançados de Otimização
Padrão 1: Composição de Componentes (Children como Props)
Em vez de renderizar filhos dentro de um pai com estado, passe-os como props:
// ❌ RUIM: Filhos re-renderizam quando estado do pai muda function Modal({ isOpen }) { const [position, setPosition] = useState({ x: 0, y: 0 }); if (!isOpen) return null; return ( <div style={{ top: position.y, left: position.x }}> <ExpensiveContent /> {/* Re-renderiza ao arrastar */} </div> ); } // ✅ BOM: Children passados como props não re-renderizam function Modal({ isOpen, children }) { const [position, setPosition] = useState({ x: 0, y: 0 }); if (!isOpen) return null; return ( <div style={{ top: position.y, left: position.x }}> {children} {/* Referência é estável, sem re-render */} </div> ); } // Uso: <Modal isOpen={isOpen}> <ExpensiveContent /> </Modal>
Isso funciona porque children é criado no pai do Modal, não dentro do Modal. Quando o estado de posição do Modal muda, a referência da prop children permanece a mesma.
Padrão 2: Colocação de Estado Extraindo Componentes
Quando você tem um componente com preocupações mistas—algo pesado em estado, algo pesado em props—extraia a parte com estado:
// ❌ RUIM: Posição do mouse causa re-renderização da lista inteira function ItemList({ items }) { const [mousePos, setMousePos] = useState({ x: 0, y: 0 }); return ( <div onMouseMove={e => setMousePos({ x: e.clientX, y: e.clientY })}> <Cursor position={mousePos} /> {items.map(item => ( <ExpensiveItem key={item.id} item={item} /> {/* Re-renderiza ao mover o mouse! */} ))} </div> ); } // ✅ BOM: Extrair a parte com estado function ItemList({ items }) { return ( <div> <CursorTracker /> {/* Contém seu próprio estado */} {items.map(item => ( <ExpensiveItem key={item.id} item={item} /> ))} </div> ); } function CursorTracker() { const [mousePos, setMousePos] = useState({ x: 0, y: 0 }); return ( <div onMouseMove={e => setMousePos({ x: e.clientX, y: e.clientY })}> <Cursor position={mousePos} /> </div> ); }
Padrão 3: Consumers de Context Seletivos
Quando você só precisa de parte de um valor de context, crie um hook customizado que se inscreve seletivamente:
// Problema: useContext re-renderiza quando QUALQUER parte do context muda function UserAvatar() { const { user } = useContext(AppContext); // Re-renderiza quando notifications muda, theme muda, etc. return <img src={user.avatar} />; } // Solução: Usar uma biblioteca de gerenciamento de estado com selectors // (Zustand, Jotai, ou Redux com selectors) import { create } from 'zustand'; const useStore = create((set) => ({ user: null, theme: 'light', notifications: [], setUser: (user) => set({ user }), })); function UserAvatar() { // Só re-renderiza quando user muda const user = useStore((state) => state.user); return <img src={user?.avatar} />; }
Padrão 4: Virtualização para Listas Longas
Se você está renderizando uma lista com 100+ itens, virtualize:
// ❌ RUIM: Renderiza todos os 10,000 itens function MessageList({ messages }) { return ( <div className="messages"> {messages.map(msg => ( <Message key={msg.id} message={msg} /> ))} </div> ); } // ✅ BOM: Só renderiza itens visíveis import { Virtuoso } from 'react-virtuoso'; function MessageList({ messages }) { return ( <Virtuoso data={messages} itemContent={(index, msg) => <Message message={msg} />} /> ); }
Bibliotecas de virtualização populares:
- react-virtuoso: Excelente para interfaces tipo chat
- @tanstack/react-virtual: Headless, flexível
- react-window: Leve, testada em batalha
Quando NÃO Otimizar
Otimização de performance tem custos:
- Complexidade do código aumenta
- Debugging fica mais difícil
- Bugs podem ser introduzidos
- Otimização prematura desperdiça tempo
Não otimize se:
- O componente renderiza rápido (< 16ms)
- O componente raramente re-renderiza
- Usuários não reclamaram de performance
- Você não tem dados do profiler mostrando que é um problema
React é rápido por padrão. O algoritmo de diffing do Virtual DOM é altamente otimizado. A maioria das re-renderizações são baratas. Só otimize quando tiver evidência de um problema.
A Regra dos 80%: Resultados do Mundo Real
Na nossa aplicação de produção, aplicamos esses princípios sistematicamente:
Antes:
- Média de 847 renderizações de componentes por interação de usuário
- Latência de input de 120ms
- Quedas de frame durante scroll
Mudanças Feitas:
- Movemos estado do formulário para os formulários (-40% renders)
- Dividimos um mega-context em 5 contexts focados (-25% renders)
- Memoizamos cálculos caros de itens de lista (-10% renders)
- Virtualizamos a lista principal de mensagens (-15% renders, eliminamos travadas de scroll)
Depois:
- Média de 156 renderizações de componentes por interação de usuário (redução de 81%)
- Latência de input de 12ms
- Scroll suave a 60fps
As correções levaram 2 dias para implementar. O profiling levou 1 dia. Entender o problema foi a parte difícil.
Checklist de Debugging
Quando encontrar um problema de performance, siga esta checklist:
- Profile primeiro - Use o React DevTools Profiler para identificar o problema real
- Verifique props de novos objetos/arrays - Estes são os culpados mais comuns
- Olhe o uso de context - Um valor de context está mudando muito frequentemente?
- Verifique a localização do estado - O estado está elevado mais do que necessário?
- Cheque renderização de listas - Você está renderizando centenas de itens sem virtualização?
- Meça depois das mudanças - Sua otimização realmente ajudou?
Conclusão
Re-renderizações do React não são o inimigo—re-renderizações desnecessárias são. O framework é projetado para ser rápido por padrão, mas ele não pode ler sua mente sobre quais atualizações são significativas.
As melhores otimizações vêm de entender sua árvore de componentes:
- Coloque o estado junto com os componentes que o usam
- Divida contexts por frequência de atualização
- Use padrões de composição para isolar atualizações
- Memoize estrategicamente, não em todo lugar
- Virtualize listas longas
Mais importante: meça antes e depois. Não confie nos seus instintos—confie no profiler.
Seus usuários nunca verão quão elegante é seu código. Eles só vão sentir quão rápido seu app responde. Agora você tem as ferramentas para dar a eles essa experiência.