Conexões Supabase Realtime: Evitando Memory Leaks com WebSockets no React
A Mágica e o Perigo do Tempo Real
O Supabase Realtime permite que você escute mudanças no banco de dados (PostgreSQL) instantaneamente via WebSockets. Se uma nova mensagem for inserida no chat ou o status de um pagamento mudar para "Concluído", a tela do usuário atualiza sozinha em milissegundos — sem a necessidade de recarregar a página ou fazer consultas repetitivas (long-polling).
Apesar de ser uma ferramenta incrivelmente poderosa, ela possui uma armadilha fatal para desenvolvedores inexperientes: O Vazamento de Memória (Memory Leak).
O Erro Amador: Sockets Zumbis
Quando você manda o navegador do cliente abrir um canal WebSocket, ele cria uma conexão TCP contínua, pesada e persistente com o servidor do Supabase.
O maior erro em aplicações Single Page (SPA) como Next.js ou React é assinar o canal e não avisar o navegador que ele deve ser destruído quando o usuário sai da página.
Como NÃO Fazer ❌
Se você escrever o código de escuta sem uma estratégia de limpeza, o vazamento acontece instantaneamente:
// ARQUITETURA PROIBIDA: Sem Cleanup
useEffect(() => {
// O usuário entra na página e abre um Socket.
supabase
.channel('room_1')
.on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'messages' }, (payload) => {
console.log('Nova mensagem', payload);
})
.subscribe();
// Se o usuário clicar em "Voltar" e depois "Entrar" novamente,
// um SEGUNDO socket é aberto, rodando em paralelo com o primeiro.
}, []);
O Resultado: Se o usuário navegar por 10 telas diferentes que assinam esse canal, o computador dele ficará com 10 conexões abertas em paralelo, processando e duplicando os mesmos dados até travar o navegador (Out of Memory) e esgotar os limites de conexão simultânea da sua conta Supabase.
A Arquitetura Padrão Ouro: Cleanups no React
No React, o useEffect possui um mecanismo de autodefesa chamado Cleanup Function. Sempre que o componente for destruído (ou seja, quando o usuário sair da página ou a tela mudar), o React vai rodar a função que você retornar lá no final.
Para que sua aplicação escale sem bugs e vazamentos de memória, é obrigatório desinscrever e remover o canal do Supabase removeChannel() antes que a tela suma.
Como FAZER ✅
Copie esta estrutura para qualquer implementação de Realtime. É a única abordagem permitida em nossa engenharia:
import { useEffect, useState } from 'react';
import { createClient } from '@/utils/supabase/client';
export function RealtimeChat() {
const [messages, setMessages] = useState([]);
const supabase = createClient();
useEffect(() => {
// 1. Criamos a referência para a conexão
const channel = supabase
.channel('chat_publico')
.on('postgres_changes',
{ event: 'INSERT', schema: 'public', table: 'messages' },
(payload) => {
// Atualiza o estado com o novo dado do banco
setMessages((prev) => [...prev, payload.new]);
})
.subscribe();
// 2. A Regra de Ouro: Função de Limpeza (Cleanup)
return () => {
// Quando o componente for desmontado (usuário mudar de página),
// matamos a conexão TCP WebSocket instantaneamente.
supabase.removeChannel(channel);
};
}, []); // Array vazio garante que o setup e o cleanup rodem 1 vez.
return (
<div>
{/* Renderiza a UI... */}
</div>
);
}
A Armadilha Avançada: Sessões, StrictMode e E2E
Mesmo fazendo o cleanup perfeitamente, você pode enfrentar três cenários catastróficos em produção ou nos testes automatizados:
1. Perda de Credenciais (setAuth)
Se você tem RLS protegendo sua tabela (só administradores podem ver mudanças), o Realtime precisa saber quem abriu o WebSocket. No React, às vezes o cliente Supabase perde o contexto da sessão. Regra de Ouro: Sempre injete o token JWT manualmente antes de assinar o canal:
const { data: { session } } = await supabase.auth.getSession();
if (session?.access_token) {
supabase.realtime.setAuth(session.access_token);
}
2. O Dobro Fantasma do StrictMode
Em desenvolvimento local, o React 18 executa useEffect duas vezes para detectar vazamentos (o famoso StrictMode). Isso significa que seu código abre, fecha e reabre o canal em milissegundos. Se você não usar o .removeChannel() perfeitamente, terá fantasmas. Em alguns casos, pode ser necessário remover canais passados pelo nome antes de criar o novo.
3. A Falha Cega do Playwright (Race Condition)
Ao escrever testes E2E (Playwright) para validar se o Realtime está funcionando, o robô é rápido demais. Ele clica num botão e faz o UPDATE no banco antes que o WebSocket do frontend tenha tido os 500ms necessários para se conectar ao servidor Supabase. O evento se perde no vácuo e o teste falha "sem motivo".
A Cura: Nos seus testes E2E, adicione sempre uma micro-pausa artificial (await page.waitForTimeout(2000)) logo após a tela carregar, permitindo o aperto de mãos (handshake) do WebSocket, para só depois simular a alteração no banco.
Resumo Operacional
- Abra o Canal: Use
supabase.channel().subscribe(). - Injete a Sessão: Use
supabase.realtime.setAuth(session.access_token)se houver RLS. - Guarde na Memória: Armazene esse canal em uma variável (
const channel). - Mate a Conexão: Retorne a função de cleanup do
useEffectexecutandosupabase.removeChannel(channel).
Com essa disciplina arquitetural, seu SaaS pode ter 100 mil usuários simultâneos usando Realtime, pois cada cliente fechará a porta ao sair, mantendo a memória do navegador imaculada e os servidores aliviados.