Voltar ao blog
Capa do artigo: Performance em SaaS: React Query + Memorização para eliminar re-renders
React QueryPerformanceReactZustand

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.

10 de fevereiro de 20257 min de leitura

Dashboard de performance

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:

  1. O cálculo é realmente custoso (filtrar/ordenar arrays grandes)
  2. O valor é passado como prop para componente memoizado com React.memo
  3. É dependência de outro useEffect ou useCallback
// ✅ 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.