Índice
1. Introdução
A performance é um dos aspectos mais críticos no desenvolvimento de aplicações React modernas. Com o crescimento da complexidade das aplicações web e as expectativas cada vez maiores dos usuários, otimizar a performance não é mais um luxo, mas uma necessidade fundamental.
Neste artigo, exploraremos técnicas avançadas de otimização que vão além dos conceitos básicos, fornecendo estratégias práticas para criar aplicações React verdadeiramente escaláveis e eficientes.
Por que a Performance Importa?
- Experiência do Usuário: Aplicações lentas frustram usuários e aumentam a taxa de abandono
- SEO: Google considera a velocidade como fator de ranking
- Conversões: Cada segundo de atraso pode reduzir conversões em até 7%
- Custos: Aplicações otimizadas consomem menos recursos de servidor
2. Fundamentos da Performance em React
2.1 Como o React Funciona
Para otimizar efetivamente, é crucial entender como o React opera internamente:
- Virtual DOM: Representação em memória da UI real
- Reconciliação: Processo de comparação e atualização do DOM
- Fiber: Arquitetura que permite interrupção e priorização de tarefas
- Batching: Agrupamento de atualizações de estado
2.2 Métricas de Performance
Antes de otimizar, precisamos medir. As principais métricas incluem:
// Exemplo de medição de performance
const startTime = performance.now();
// Operação a ser medida
const result = heavyComputation();
const endTime = performance.now();
console.log(`Operação levou ${endTime - startTime} milissegundos`);
// Usando React DevTools Profiler
import { Profiler } from 'react';
function onRenderCallback(id, phase, actualDuration) {
console.log('Componente:', id);
console.log('Fase:', phase);
console.log('Duração:', actualDuration);
}
<Profiler id="App" onRender={onRenderCallback}>
<App />
</Profiler>
3. Técnicas de Memoização
3.1 React.memo
O React.memo é uma Higher-Order Component que memoriza o resultado de um componente, evitando re-renderizações desnecessárias:
// Componente sem otimização
const ExpensiveComponent = ({ data, config }) => {
const processedData = processData(data, config);
return <div>{processedData}</div>;
};
// Componente otimizado com React.memo
const OptimizedComponent = React.memo(({ data, config }) => {
const processedData = processData(data, config);
return <div>{processedData}</div>;
});
// Comparação customizada
const OptimizedComponentCustom = React.memo(
({ data, config }) => {
const processedData = processData(data, config);
return <div>{processedData}</div>;
},
(prevProps, nextProps) => {
// Retorna true se as props são iguais (não deve re-renderizar)
return (
prevProps.data.id === nextProps.data.id &&
prevProps.config.version === nextProps.config.version
);
}
);
3.2 useMemo Hook
O useMemo memoriza o resultado de cálculos custosos:
import { useMemo } from 'react';
const DataVisualization = ({ rawData, filters }) => {
// Cálculo custoso memorizado
const processedData = useMemo(() => {
console.log('Processando dados...');
return rawData
.filter(item => filters.includes(item.category))
.map(item => ({
...item,
computed: heavyComputation(item.value)
}))
.sort((a, b) => b.computed - a.computed);
}, [rawData, filters]); // Dependências
// Gráfico memorizado
const chartConfig = useMemo(() => {
return {
type: 'bar',
data: processedData,
options: {
responsive: true,
plugins: {
legend: { position: 'top' }
}
}
};
}, [processedData]);
return <Chart config={chartConfig} />;
};
3.3 useCallback Hook
O useCallback memoriza funções, evitando recriações desnecessárias:
import { useCallback, useState } from 'react';
const TodoList = ({ todos }) => {
const [filter, setFilter] = useState('all');
// Função memorizada
const handleToggle = useCallback((id) => {
setTodos(prev => prev.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
}, []); // Sem dependências pois usa função de atualização
// Função com dependências
const handleFilter = useCallback((newFilter) => {
setFilter(newFilter);
analytics.track('filter_changed', { filter: newFilter });
}, []); // Analytics é estável
const filteredTodos = useMemo(() => {
switch (filter) {
case 'active':
return todos.filter(todo => !todo.completed);
case 'completed':
return todos.filter(todo => todo.completed);
default:
return todos;
}
}, [todos, filter]);
return (
<div>
<FilterButtons onFilter={handleFilter} />
{filteredTodos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={handleToggle}
/>
))}
</div>
);
};
4. Code Splitting e Lazy Loading
4.1 Lazy Loading de Componentes
Carregue componentes apenas quando necessário:
import { lazy, Suspense } from 'react';
// Lazy loading de componentes
const Dashboard = lazy(() => import('./Dashboard'));
const Profile = lazy(() => import('./Profile'));
const Settings = lazy(() => import('./Settings'));
// Componente com fallback customizado
const LazyChart = lazy(() =>
import('./Chart').then(module => ({
default: module.Chart
}))
);
const App = () => {
return (
<Router>
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/profile" element={<Profile />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
</Router>
);
};
// Preload estratégico
const preloadDashboard = () => {
import('./Dashboard');
};
// Preload no hover
<Link
to="/dashboard"
onMouseEnter={preloadDashboard}
>
Dashboard
</Link>
4.2 Dynamic Imports Avançados
Técnicas avançadas de importação dinâmica:
// Carregamento condicional
const loadFeature = async (featureName) => {
switch (featureName) {
case 'analytics':
const { AnalyticsModule } = await import('./features/Analytics');
return AnalyticsModule;
case 'reporting':
const { ReportingModule } = await import('./features/Reporting');
return ReportingModule;
default:
throw new Error(`Feature ${featureName} not found`);
}
};
// Hook customizado para lazy loading
const useLazyComponent = (importFunc) => {
const [Component, setComponent] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const loadComponent = useCallback(async () => {
try {
setLoading(true);
setError(null);
const module = await importFunc();
setComponent(() => module.default || module);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
}, [importFunc]);
return { Component, loading, error, loadComponent };
};
// Uso do hook
const FeatureLoader = ({ featureName }) => {
const { Component, loading, error, loadComponent } = useLazyComponent(
() => import(`./features/${featureName}`)
);
if (error) return <ErrorBoundary error={error} />;
if (loading) return <FeatureLoadingSkeleton />;
if (!Component) {
return <button onClick={loadComponent}>Load Feature</button>;
}
return <Component />;
};
5. Virtualização de Listas
5.1 React Window
Para listas grandes, a virtualização é essencial:
import { FixedSizeList as List } from 'react-window';
// Componente de item da lista
const ListItem = ({ index, style, data }) => (
<div style={style}>
<div className="list-item">
<h3>{data[index].title}</h3>
<p>{data[index].description}</p>
</div>
</div>
);
// Lista virtualizada
const VirtualizedList = ({ items }) => (
<List
height={600} // Altura do container
itemCount={items.length}
itemSize={120} // Altura de cada item
itemData={items} // Dados passados para cada item
>
{ListItem}
</List>
);
// Lista com tamanhos variáveis
import { VariableSizeList } from 'react-window';
const getItemSize = (index) => {
// Lógica para calcular altura baseada no conteúdo
return items[index].type === 'header' ? 60 : 120;
};
const VariableList = ({ items }) => (
<VariableSizeList
height={600}
itemCount={items.length}
itemSize={getItemSize}
itemData={items}
>
{ListItem}
</VariableSizeList>
);
5.2 Virtualização Customizada
Implementação de virtualização personalizada:
const useVirtualization = ({
items,
itemHeight,
containerHeight,
overscan = 5
}) => {
const [scrollTop, setScrollTop] = useState(0);
const visibleStart = Math.floor(scrollTop / itemHeight);
const visibleEnd = Math.min(
visibleStart + Math.ceil(containerHeight / itemHeight),
items.length - 1
);
const startIndex = Math.max(0, visibleStart - overscan);
const endIndex = Math.min(items.length - 1, visibleEnd + overscan);
const visibleItems = items.slice(startIndex, endIndex + 1);
const totalHeight = items.length * itemHeight;
const offsetY = startIndex * itemHeight;
return {
visibleItems,
totalHeight,
offsetY,
startIndex,
setScrollTop
};
};
const CustomVirtualList = ({ items, itemHeight = 50 }) => {
const containerRef = useRef();
const [containerHeight, setContainerHeight] = useState(400);
const {
visibleItems,
totalHeight,
offsetY,
startIndex,
setScrollTop
} = useVirtualization({
items,
itemHeight,
containerHeight
});
const handleScroll = (e) => {
setScrollTop(e.target.scrollTop);
};
return (
<div
ref={containerRef}
style={{ height: containerHeight, overflow: 'auto' }}
onScroll={handleScroll}
>
<div style={{ height: totalHeight, position: 'relative' }}>
<div style={{ transform: `translateY(${offsetY}px)` }}>
{visibleItems.map((item, index) => (
<div
key={startIndex + index}
style={{ height: itemHeight }}
>
{item.content}
</div>
))}
</div>
</div>
</div>
);
};
6. Otimização de Estado
6.1 Estrutura de Estado Eficiente
Como organizar o estado para máxima performance:
// ❌ Estado mal estruturado
const [appState, setAppState] = useState({
user: { id: 1, name: 'João' },
todos: [...],
ui: { loading: false, modal: null },
cache: { ... }
});
// ✅ Estado bem estruturado
const [user, setUser] = useState({ id: 1, name: 'João' });
const [todos, setTodos] = useState([]);
const [ui, setUI] = useState({ loading: false, modal: null });
// Ou usando useReducer para lógica complexa
const initialState = {
todos: [],
filter: 'all',
loading: false
};
const todoReducer = (state, action) => {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [...state.todos, action.payload]
};
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload
? { ...todo, completed: !todo.completed }
: todo
)
};
case 'SET_FILTER':
return {
...state,
filter: action.payload
};
default:
return state;
}
};
const TodoApp = () => {
const [state, dispatch] = useReducer(todoReducer, initialState);
// Actions memoizadas
const actions = useMemo(() => ({
addTodo: (text) => dispatch({
type: 'ADD_TODO',
payload: { id: Date.now(), text, completed: false }
}),
toggleTodo: (id) => dispatch({
type: 'TOGGLE_TODO',
payload: id
}),
setFilter: (filter) => dispatch({
type: 'SET_FILTER',
payload: filter
})
}), []);
return (
<TodoContext.Provider value={{ state, actions }}>
<TodoList />
</TodoContext.Provider>
);
};
6.2 Context Optimization
Otimizando o uso de Context para evitar re-renderizações desnecessárias:
// ❌ Context que causa muitas re-renderizações
const AppContext = createContext();
const AppProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
const [notifications, setNotifications] = useState([]);
// Objeto recriado a cada render
const value = {
user, setUser,
theme, setTheme,
notifications, setNotifications
};
return (
<AppContext.Provider value={value}>
{children}
</AppContext.Provider>
);
};
// ✅ Context otimizado com separação de responsabilidades
const UserContext = createContext();
const ThemeContext = createContext();
const NotificationContext = createContext();
const UserProvider = ({ children }) => {
const [user, setUser] = useState(null);
const value = useMemo(() => ({
user,
setUser
}), [user]);
return (
<UserContext.Provider value={value}>
{children}
</UserContext.Provider>
);
};
// Hook customizado com seletor
const useUserSelector = (selector) => {
const context = useContext(UserContext);
return useMemo(() => selector(context), [context, selector]);
};
// Uso otimizado
const UserProfile = () => {
const userName = useUserSelector(ctx => ctx.user?.name);
return <div>{userName}</div>;
};
7. Ferramentas de Profiling
7.1 React DevTools Profiler
Como usar o Profiler para identificar gargalos:
// Profiler programático
import { Profiler } from 'react';
const onRenderCallback = (id, phase, actualDuration, baseDuration, startTime, commitTime) => {
// Enviar métricas para serviço de analytics
analytics.track('component_render', {
componentId: id,
phase, // 'mount' ou 'update'
actualDuration, // Tempo real gasto
baseDuration, // Tempo estimado sem otimizações
startTime,
commitTime
});
// Log para desenvolvimento
if (process.env.NODE_ENV === 'development') {
console.group(`🔍 Profiler: ${id}`);
console.log(`Fase: ${phase}`);
console.log(`Duração real: ${actualDuration}ms`);
console.log(`Duração base: ${baseDuration}ms`);
console.groupEnd();
}
};
const App = () => (
<Profiler id="App" onRender={onRenderCallback}>
<Header />
<Profiler id="MainContent" onRender={onRenderCallback}>
<MainContent />
</Profiler>
<Footer />
</Profiler>
);
7.2 Performance Monitoring
Implementando monitoramento contínuo de performance:
// Hook para monitoramento de performance
const usePerformanceMonitor = (componentName) => {
const renderCount = useRef(0);
const lastRenderTime = useRef(performance.now());
useEffect(() => {
renderCount.current += 1;
const currentTime = performance.now();
const timeSinceLastRender = currentTime - lastRenderTime.current;
// Alertar sobre renders muito frequentes
if (timeSinceLastRender < 16) { // Menos que 1 frame (60fps)
console.warn(`⚠️ ${componentName} renderizando muito frequentemente`);
}
lastRenderTime.current = currentTime;
// Log periódico
if (renderCount.current % 10 === 0) {
console.log(`📊 ${componentName} renderizou ${renderCount.current} vezes`);
}
});
return renderCount.current;
};
// Componente com monitoramento
const MonitoredComponent = ({ data }) => {
const renderCount = usePerformanceMonitor('MonitoredComponent');
return (
<div>
<span>Renders: {renderCount}</span>
{/* Conteúdo do componente */}
</div>
);
};
8. Melhores Práticas
8.1 Checklist de Performance
8.2 Padrões Anti-Performance
Evite estes padrões comuns que prejudicam a performance:
// ❌ Criando objetos/arrays inline
<Component style={{ marginTop: 10 }} data={[1, 2, 3]} />
// ✅ Definindo fora do render
const styles = { marginTop: 10 };
const data = [1, 2, 3];
<Component style={styles} data={data} />
// ❌ Funções inline em props
<button onClick={() => handleClick(id)}>Click</button>
// ✅ Usando useCallback
const handleButtonClick = useCallback(() => handleClick(id), [id]);
<button onClick={handleButtonClick}>Click</button>
// ❌ Condicionais complexas no JSX
{user && user.permissions && user.permissions.includes('admin') &&
user.settings && user.settings.showAdminPanel && <AdminPanel />}
// ✅ Computação prévia
const showAdminPanel = useMemo(() =>
user?.permissions?.includes('admin') &&
user?.settings?.showAdminPanel,
[user]
);
{showAdminPanel && <AdminPanel />}
9. Conclusão
A otimização de performance em React é um processo contínuo que requer compreensão profunda dos mecanismos internos do framework e aplicação cuidadosa das técnicas apropriadas.
As estratégias apresentadas neste artigo - desde memoização até virtualização - devem ser aplicadas de forma criteriosa, sempre baseadas em medições reais de performance e não em otimizações prematuras.
Principais Pontos:
- Meça antes de otimizar: Use ferramentas de profiling para identificar gargalos reais
- Otimize progressivamente: Comece com as técnicas de maior impacto
- Monitore continuamente: Implemente métricas de performance em produção
- Mantenha o equilíbrio: Performance não deve comprometer a manutenibilidade do código
Lembre-se: uma aplicação React bem otimizada não apenas oferece melhor experiência ao usuário, mas também reduz custos de infraestrutura e melhora o posicionamento nos mecanismos de busca.