Pular para o conteúdo
Guias Práticos

Como identificar gargalos de performance em Node.js sem reescrever tudo

Antes de migrar para Go ou Rust, descubra onde o seu Node.js realmente trava. Um roteiro de profiling em produção que encontra a causa real, não o palpite.

Guias PráticosJohnny Carreiro·13 de janeiro de 2026·3 min de leitura

Toda vez que uma API em Node.js fica lenta, surge a mesma sugestão de corredor: "Node não escala, migra para Go." Na prática, na enorme maioria dos casos, o problema não é a linguagem — é o que o seu código faz com ela. Antes de cogitar uma reescrita de meses, vale gastar algumas horas descobrindo onde o tempo realmente vai.

Profiling em produção, não em dev

O primeiro erro é medir no ambiente errado. Em dev, sem carga real, sem o volume de dados de produção e sem concorrência, os números mentem. Um endpoint que parece instantâneo na sua máquina pode estar bloqueando o event loop sob 200 requisições simultâneas.

Comece com observabilidade: tempos de resposta por rota (P50, P95, P99), uso de CPU e memória, e contagem de queries por requisição. Ferramentas como clinic.js, 0x (para flamegraphs) e o próprio node --inspect com heap snapshots dão a fotografia que você precisa — idealmente sobre tráfego real ou um replay dele.

Os quatro suspeitos de sempre

Na prática, gargalos de Node.js quase sempre caem em um destes quatro padrões.

Event loop bloqueado. Node é single-threaded para o seu código. Uma operação síncrona pesada — um JSON.parse de um payload gigante, um crypto síncrono, um loop sobre milhares de itens — congela todas as outras requisições enquanto roda. O sintoma é P99 disparando enquanto a média parece ok. O flamegraph aponta a função culpada.

N+1 no banco. O ORM esconde queries. Listar 100 pedidos e, para cada um, buscar o cliente, gera 101 queries. Sob carga, o banco vira o gargalo e o Node só espera. Logar a contagem de queries por requisição revela isso na hora.

Memory leak. Um cache que nunca expira, um array de listeners que só cresce, um closure segurando referência. A memória sobe degrau a degrau e o garbage collector pausa cada vez mais. Heap snapshots tirados em momentos diferentes, comparados, mostram o que não está sendo liberado.

Connection leak no pool. Conexões de banco que não voltam ao pool esgotam o limite e as requisições passam a esperar por uma conexão livre. O sintoma é latência crescente sem CPU alta — o processo está apenas esperando.

A regra dos 80/20

Resista à vontade de otimizar tudo. Em quase todo sistema, três a cinco causas explicam 80% do problema. O flamegraph e os logs de query mostram quais são. Conserte essas, meça de novo, e só então decida se vale ir além.

Um exemplo concreto: já vimos uma API com P99 de 8 segundos cair para menos de 400ms só corrigindo um N+1 e adicionando um índice — sem trocar de linguagem, sem reescrever nada. A migração para Go que o time considerava teria custado meses e provavelmente recriado os mesmos N+1 em outra sintaxe.

Quando a reescrita realmente faz sentido

Há casos legítimos: workloads CPU-bound puros (processamento de imagem, criptografia pesada, parsing de grandes volumes) onde o single-thread é limite real. Mas esses são minoria, e mesmo neles a saída costuma ser um worker thread ou um serviço dedicado para a parte quente — não reescrever a aplicação inteira.

O checklist antes de decidir

Antes de aprovar qualquer reescrita por performance, passe por: flamegraph sob carga real, contagem de queries por requisição, heap snapshots comparados, métricas do pool de conexões e P99 por rota. Se você não tem esses cinco números, ainda não sabe onde está o problema — só tem um palpite caro.

Performance é diagnóstico antes de cirurgia. A reescrita é a opção mais cara e mais arriscada; quase sempre, existe um fix cirúrgico que devolve a maior parte do ganho por uma fração do custo.