Dicas para depurar problemas de memória em Node.js
1. Entendendo o Gerenciamento de Memória no Node.js
O Node.js utiliza o motor V8 do Google para executar JavaScript, que implementa um garbage collector (GC) sofisticado. O gerenciamento de memória no V8 divide-se em duas áreas principais: heap (onde objetos e closures são alocados) e stack (onde variáveis locais e chamadas de função residem). O GC opera em gerações: objetos jovens (young generation) são coletados rapidamente, enquanto objetos que sobrevivem a múltiplas coleções são promovidos para a old generation.
Referências fortes mantêm objetos vivos, enquanto referências fracas (WeakRef) permitem que o GC colete objetos se não houver outras referências fortes. Entender esse ciclo é fundamental para depurar vazamentos.
// Exemplo: monitorando uso de memória
const memUsage = process.memoryUsage();
console.log('RSS:', memUsage.rss);
console.log('Heap Total:', memUsage.heapTotal);
console.log('Heap Used:', memUsage.heapUsed);
console.log('External:', memUsage.external);
2. Identificando Sintomas de Vazamento de Memória
O primeiro passo para depurar problemas de memória é reconhecer os sintomas. Use process.memoryUsage() para monitorar o consumo ao longo do tempo. Sinais comuns incluem crescimento contínuo do heap mesmo após a coleta de lixo, degradação progressiva de performance e aumento do RSS (Resident Set Size).
O Node.js oferece flags nativas úteis:
# Ativar rastreamento do GC
node --trace-gc app.js
# Limitar o tamanho máximo do heap (em MB)
node --max-old-space-size=512 app.js
Um padrão suspeito é quando o heap usado nunca retorna ao nível anterior após picos de alocação:
setInterval(() => {
const usage = process.memoryUsage();
console.log(`Heap usado: ${(usage.heapUsed / 1024 / 1024).toFixed(2)} MB`);
}, 2000);
3. Capturando e Analisando Heap Dumps
Heap dumps são instantâneos da memória que permitem inspecionar objetos retidos. O módulo heapdump gera snapshots sob demanda:
const heapdump = require('heapdump');
const fs = require('fs');
// Gerar snapshot manualmente
heapdump.writeSnapshot(`./heap-${Date.now()}.heapsnapshot`);
// Ou sob condição
setInterval(() => {
if (process.memoryUsage().heapUsed > 500 * 1024 * 1024) {
heapdump.writeSnapshot(`./heap-critico-${Date.now()}.heapsnapshot`);
}
}, 30000);
Para análise, carregue o arquivo .heapsnapshot no Chrome DevTools (aba Memory). Compare dois snapshots — um antes e outro depois de uma operação — para identificar objetos que não foram liberados.
Ferramentas como clinic.js automatizam esse processo:
npx clinic heapprofiler -- node app.js
4. Detectando Vazamentos com Ferramentas de Profiling
O modo --inspect permite conectar o Chrome DevTools diretamente ao processo Node.js:
node --inspect app.js
No DevTools, use a aba Memory para:
- Gravar alocações em tempo real (Allocation sampling)
- Identificar closures que retêm escopos
- Detectar listeners de eventos não removidos
Flamegraphs com a ferramenta 0x revelam onde a CPU está sendo gasta, mas também podem indicar vazamentos indiretos:
npx 0x app.js
Padrões comuns de vazamento incluem:
// Vazamento por closure
function criarVazamento() {
const dadosPesados = new Array(1000000).fill('x');
return function() {
console.log(dadosPesados.length); // Closure retém dadosPesados
};
}
// Vazamento por listener não removido
const EventEmitter = require('events');
const emissor = new EventEmitter();
function registrarListener() {
emissor.on('evento', () => {
// Este listener nunca é removido
console.log('evento recebido');
});
}
5. Estratégias para Corrigir Vazamentos Comuns
Para timers e intervalos, sempre armazene e limpe as referências:
const intervalId = setInterval(() => {
// lógica
}, 1000);
// Quando não for mais necessário
clearInterval(intervalId);
Para EventEmitters, remova listeners explicitamente:
function handler() {
console.log('evento tratado');
}
emissor.on('evento', handler);
// Remover quando não precisar mais
emissor.off('evento', handler);
Caches e mapas devem ser controlados com limites de tamanho:
const cache = new Map();
const MAX_CACHE = 100;
function adicionarAoCache(chave, valor) {
if (cache.size >= MAX_CACHE) {
const primeiraChave = cache.keys().next().value;
cache.delete(primeiraChave);
}
cache.set(chave, valor);
}
Para objetos temporários, use WeakRef e FinalizationRegistry:
const registry = new FinalizationRegistry((valor) => {
console.log(`Objeto ${valor} foi coletado`);
});
let objeto = { dados: 'temporario' };
const ref = new WeakRef(objeto);
registry.register(objeto, 'meu-objeto');
// Quando objeto for sobrescrito, o GC pode coletá-lo
objeto = null;
6. Otimizando o Garbage Collector
Ajustes no GC podem reduzir pausas e melhorar performance:
# Coleta global mais agressiva
node --gc-global app.js
# Desabilitar sweeping concorrente para debug
node --noconcurrent_sweeping app.js
# Otimizar para tamanho (reduz fragmentação)
node --optimize_for_size app.js
# Ajustar tamanho do semi-space (espaço jovem)
node --max_semi_space_size=64 app.js
Evite padrões que forçam GC excessivo:
// Ruim: alocação em loop
for (let i = 0; i < 100000; i++) {
const temp = new Array(1000);
}
// Bom: reutilizar objeto
const buffer = new Array(1000);
for (let i = 0; i < 100000; i++) {
buffer.fill(0);
// usar buffer
}
7. Boas Práticas para Prevenir Problemas Futuros
Implemente limites de memória no código:
const MAX_HEAP = 400 * 1024 * 1024; // 400 MB
function verificarMemoria() {
const usado = process.memoryUsage().heapUsed;
if (usado > MAX_HEAP) {
console.error('Memória excedida. Reiniciando...');
process.exit(1);
}
}
setInterval(verificarMemoria, 10000);
Monitore continuamente com APM como New Relic ou Datadog:
# Exemplo com clinic.js para testes de estresse
npx clinic doctor -- node app.js
Revise o código focando em:
- Escopo de variáveis (evite globais)
- Ciclo de vida de objetos (liberação adequada)
- Uso de streams em vez de buffers grandes
- Implementação de backpressure em operações assíncronas
Referências
- Documentação oficial do Node.js sobre diagnóstico de memória — Guia completo sobre ferramentas e técnicas para diagnóstico de memória no Node.js
- Chrome DevTools Memory Panel — Tutorial oficial sobre como usar o painel de memória para identificar vazamentos
- Artigo sobre vazamentos de memória no Node.js — Exemplos práticos de vazamentos comuns e como corrigi-los
- Documentação do módulo heapdump — Guia de uso para gerar snapshots de heap no Node.js
- Clinic.js - Ferramenta de profiling — Documentação oficial da suíte de ferramentas para diagnóstico de performance e memória
- WeakRef e FinalizationRegistry no MDN — Referência técnica sobre referências fracas e registro de finalização em JavaScript
- Guia de otimização de GC no V8 — Artigo do blog oficial do V8 sobre estratégias de garbage collection