Performance em SaaS: React Query + Memorização para eliminar re-renders
Técnicas reais aplicadas em produção para otimizar renderização em aplicações SaaS densas usando React Query, useMemo e useCallback.
O problema dos re-renders em SaaS
Uma das maiores batalhas que tive na SenseData foi lidar com uma aplicação SaaS densa que re-renderizava componentes desnecessariamente, causando travamentos perceptíveis ao usuário.
O problema fundamental: estado global mal gerenciado + cache inexistente + componentes sem memoização.
React Query como cache de servidor
Antes do React Query, o padrão era algo assim:
// ❌ Padrão sem cache — toda navegação refaz a requisição
function Dashboard() {
const [data, setData] = useState(null);
useEffect(() => {
fetch('/api/dashboard').then(r => r.json()).then(setData);
}, []);
// ...
}
Com React Query:
// ✅ Cache automático, revalidação inteligente
function Dashboard() {
const { data, isLoading } = useQuery({
queryKey: ['dashboard'],
queryFn: fetchDashboard,
staleTime: 5 * 60 * 1000, // 5 minutos frescos
gcTime: 10 * 60 * 1000, // 10 minutos no cache
});
// ...
}
O usuário navega entre páginas e volta ao dashboard instantaneamente porque os dados já estão no cache.
Granularidade de queryKey
A chave do cache é crítica. Errar aqui causa dados desatualizados ou invalidações desnecessárias:
// ❌ Chave genérica demais — invalida tudo ao atualizar qualquer user
queryKey: ['users']
// ✅ Granular — invalida só o user específico
queryKey: ['users', userId]
// ✅ Com filtros — cache separado por contexto
queryKey: ['users', { status: 'active', page: 1 }]
select para transformação sem re-render
O select transforma dados sem causar re-render quando apenas partes não selecionadas mudam:
// Só re-renderiza quando os nomes mudam, não quando outros campos do user mudam
const { data: userNames } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
select: (users) => users.map(u => u.name),
});
Zustand para estado de UI local
React Query cuida do estado do servidor. Para estado de UI (modais abertos, filtros, tabs ativas), Zustand é a escolha certa:
interface UIStore {
activeTab: string;
sidebarOpen: boolean;
setActiveTab: (tab: string) => void;
toggleSidebar: () => void;
}
const useUIStore = create<UIStore>((set) => ({
activeTab: 'overview',
sidebarOpen: true,
setActiveTab: (tab) => set({ activeTab: tab }),
toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })),
}));
useMemo e useCallback com criteriosidade
Memoização tem custo. Use apenas quando:
- O cálculo é realmente custoso (filtrar/ordenar arrays grandes)
- O valor é passado como prop para componente memoizado com
React.memo - É dependência de outro
useEffectouuseCallback
// ✅ Faz sentido — filtragem de lista grande
const filteredItems = useMemo(
() => items.filter(item => item.status === activeFilter),
[items, activeFilter]
);
// ❌ Desnecessário — cálculo trivial
const label = useMemo(() => `${firstName} ${lastName}`, [firstName, lastName]);
// Prefira: const label = `${firstName} ${lastName}`;
Resultado prático
Na SenseData, combinando essas técnicas:
- Redução de 60% nas requisições de rede (cache do React Query)
- Eliminação de re-renders em componentes de tabela com 1000+ linhas
- SLA de bugs de performance: de 10 para 3 dias de resolução
Performance não é um recurso, é uma funcionalidade.