Otimizar uma aplicação sem medir pode transformar uma suspeita em uma alteração inútil. O módulo Performance Hooks no Node.js, disponível em node:perf_hooks, fornece relógios de alta resolução, marcas, medidas, observadores, histogramas e métricas do event loop para investigar o comportamento real do processo.
Essas APIs ajudam a responder perguntas práticas: quanto tempo uma consulta levou, qual etapa de inicialização é mais lenta, quanto o event loop está atrasado e se uma nova versão aumentou o custo de uma função. As medições podem ser enviadas para logs, métricas ou ferramentas de observabilidade.
Neste guia, você aprenderá a usar performance.now(), mark(), measure(), PerformanceObserver, timerify, monitoramento do event loop, histogramas e padrões seguros para instrumentar produção.
O que é node:perf_hooks?
O módulo implementa conceitos semelhantes à Performance API dos navegadores, adaptados ao ambiente Node.js. Ele oferece um relógio monotônico, independente de mudanças no horário do sistema, além de uma timeline de entradas de desempenho.
A documentação oficial de Performance Hooks lista as classes, tipos de entrada e métricas disponíveis. Para entender a base do runtime, consulte Event Loop no Node.js e o que é Node.js.
Medindo com performance.now()
performance.now() retorna milissegundos com precisão fracionária desde uma origem monotônica:
const { performance } = require('node:perf_hooks');
const start = performance.now();
performTask();
const duration = performance.now() - start;
console.log(`Duração: ${duration.toFixed(2)} ms`);Para duração, ele é preferível a Date.now(), porque ajustes de relógio, sincronização de horário ou mudança manual não fazem o valor retroceder.
Medindo código assíncrono
const start = performance.now();
await fetchData();
const duration = performance.now() - start;A medição inclui o tempo de espera da operação externa. Isso é útil para observar a experiência completa, mas não separa rede, fila, servidor remoto e processamento local. Combine métricas e traces quando precisar dessa decomposição.
Criando marcas
performance.mark() registra pontos nomeados na timeline:
performance.mark('load-start');
await loadConfiguration();
performance.mark('load-end');Marcas podem incluir detalhes:
performance.mark('query-start', {
detail: { operation: 'list-users' }
});Não coloque senhas, tokens, consultas completas ou dados pessoais em detalhes que possam ser exportados.
Criando medidas
performance.measure(
'configuration-load',
'load-start',
'load-end'
);A medida recebe duração calculada entre as marcas. Você pode recuperá-la:
const entries = performance.getEntriesByName(
'configuration-load'
);
console.log(entries[0].duration);Limpando a timeline
Entradas permanecem na timeline até serem removidas:
performance.clearMarks('load-start');
performance.clearMarks('load-end');
performance.clearMeasures('configuration-load');Uma aplicação que cria marcas por requisição e nunca limpa pode acumular memória. Prefira um observador que processe entradas e libere o histórico.
PerformanceObserver
O observador recebe entradas conforme são criadas:
const {
performance,
PerformanceObserver
} = require('node:perf_hooks');
const observer = new PerformanceObserver(list => {
for (const entry of list.getEntries()) {
console.log({
name: entry.name,
type: entry.entryType,
duration: entry.duration
});
}
});
observer.observe({ entryTypes: ['measure'] });Desconecte observadores quando não forem mais necessários:
observer.disconnect();Instrumentando funções com timerify()
performance.timerify() cria um wrapper que produz entradas do tipo function:
const calculate = performance.timerify(function calculate(data) {
return data.reduce((total, value) => total + value, 0);
});
calculate([1, 2, 3]);Configure um observador:
const observer = new PerformanceObserver(list => {
for (const entry of list.getEntries()) {
console.log(entry.name, entry.duration);
}
});
observer.observe({ entryTypes: ['function'] });Instrumentar toda função gera overhead e volume excessivo. Aplique apenas em caminhos críticos ou durante investigações controladas.
Histogramas de duração
Um histograma preserva a distribuição, permitindo observar percentis:
const { createHistogram } = require('node:perf_hooks');
const histogram = createHistogram();
histogram.record(1200000);
histogram.record(5000000);
console.log({
p50: histogram.percentile(50),
p99: histogram.percentile(99),
max: histogram.max
});Verifique a unidade esperada pela API. Misturar nanossegundos e milissegundos produz métricas sem significado.
monitorEventLoopDelay()
O atraso do event loop indica quanto a thread principal demorou além do esperado:
const { monitorEventLoopDelay } = require('node:perf_hooks');
const delay = monitorEventLoopDelay({ resolution: 20 });
delay.enable();
setInterval(() => {
console.log({
p50Ms: delay.percentile(50) / 1e6,
p99Ms: delay.percentile(99) / 1e6,
maxMs: delay.max / 1e6
});
delay.reset();
}, 10000);Um valor alto pode ser causado por cálculo síncrono, garbage collection, serialização, expressões regulares custosas ou excesso de callbacks. Correlacione com CPU e tráfego.
Event loop utilization
A utilização do event loop mede tempo ativo e ocioso:
const first = performance.eventLoopUtilization();
setTimeout(() => {
const result = performance.eventLoopUtilization(first);
console.log(result.utilization);
}, 5000);Uma utilização próxima de 1 por longos períodos pode indicar saturação. Entretanto, uma aplicação com trabalho útil intenso também pode apresentar valor alto. Interprete junto com latência e throughput.
PerformanceResourceTiming
Algumas APIs podem produzir entradas de recurso. Elas permitem observar fases de operações compatíveis, semelhante ao que ocorre no navegador. A disponibilidade depende do recurso e da versão do Node.js.
Não baseie toda observabilidade em um tipo de entrada experimental. Mantenha métricas próprias para operações de negócio importantes.
Medindo inicialização
Marcas ajudam a identificar dependências lentas:
performance.mark('bootstrap-start');
await connectDatabase();
performance.mark('database-ready');
await startServer();
performance.mark('server-ready');
performance.measure(
'database-connect',
'bootstrap-start',
'database-ready'
);
performance.measure(
'server-start',
'database-ready',
'server-ready'
);Esse diagnóstico é valioso em ambientes com autoscaling, nos quais inicializações lentas atrasam readiness e substituição de instâncias.
Medindo endpoints
Um middleware pode registrar duração total:
app.use((req, res, next) => {
const start = performance.now();
res.on('finish', () => {
const duration = performance.now() - start;
metrics.observeHttpDuration({
method: req.method,
route: req.route?.path || 'unknown',
status: res.statusCode,
duration
});
});
next();
});Use uma rota normalizada, não a URL completa, para evitar cardinalidade alta. Identificadores de usuário e parâmetros dinâmicos não devem virar labels.
Média não é suficiente
Uma média de 50 ms pode esconder algumas requisições de vários segundos. Observe p50, p90, p95, p99 e máximo. Também acompanhe quantidade de amostras, porque um percentil calculado com poucos eventos pode ser instável.
Overhead da instrumentação
Toda medição consome CPU e memória. Logs por chamada podem ser mais caros que a função medida. Use amostragem, agregação e exportação em lote. Evite transformar um diagnóstico em novo gargalo.
Performance Hooks e OpenTelemetry
Performance Hooks oferece medições locais e de baixo nível. OpenTelemetry organiza traces e métricas distribuídas entre serviços. As duas abordagens são complementares. Use performance.now() para trechos específicos e spans para acompanhar uma operação entre APIs, filas e bancos.
Veja o guia de OpenTelemetry no Node.js para instrumentação distribuída.
Testes de desempenho
Execute benchmarks com aquecimento, várias iterações e dados representativos. O compilador JIT, o garbage collector e caches afetam os resultados. Não conclua que uma abordagem é mais rápida com uma única execução.
Separe microbenchmark de teste de carga. Uma função rápida isolada pode não melhorar a latência do sistema quando o gargalo real é banco, rede ou contenção.
Erros comuns
- Usar Date.now para duração crítica: o relógio pode sofrer ajustes.
- Não limpar marcas: a timeline cresce continuamente.
- Instrumentar tudo: overhead e volume escondem o sinal.
- Observar apenas média: caudas lentas ficam invisíveis.
- Usar URL completa como label: a cardinalidade explode.
- Misturar unidades: números se tornam incorretos.
- Otimizar microbenchmark isolado: o gargalo do sistema pode ser outro.
Boas práticas para produção
- Use relógio monotônico para durações.
- Meça operações relevantes para o usuário.
- Observe percentis e quantidade de amostras.
- Limpe entradas ou processe com observadores.
- Aplique amostragem em alto volume.
- Evite dados sensíveis e labels dinâmicas.
- Monitore event loop delay e utilization.
- Correlacione métricas com CPU, memória e tráfego.
- Compare versões com a mesma carga.
- Defina objetivos de latência antes de otimizar.
Conclusão
O módulo Performance Hooks no Node.js transforma percepções em medições. Relógios monotônicos, marcas, medidas, observadores e histogramas permitem identificar trechos lentos sem depender apenas de logs genéricos.
Uma instrumentação eficiente deve ser seletiva, agregada e conectada a objetivos reais. Ao acompanhar percentis, atraso do event loop e utilização, você encontra gargalos com mais confiança e evita otimizações que não melhoram a experiência do sistema.




