O módulo node:perf_hooks reúne APIs para medir desempenho dentro de aplicações Node.js. Ele permite criar marcas e medidas, observar entradas de performance, acompanhar o event loop, registrar histogramas e coletar informações de tempo com resolução adequada para diagnóstico.
Essas APIs são mais confiáveis que espalhar chamadas de Date.now() pelo código. Elas seguem conceitos semelhantes à Performance Timeline dos navegadores e podem ser integradas a logs, métricas e testes de regressão.
Importando o módulo
import {
performance,
PerformanceObserver,
monitorEventLoopDelay,
createHistogram,
} from 'node:perf_hooks';O módulo é interno do Node.js, portanto não precisa ser instalado. Use o prefixo node: para deixar claro que a dependência pertence ao runtime.
performance.now
performance.now() retorna um valor monotônico em milissegundos. Ele é adequado para medir duração porque não sofre ajustes do relógio do sistema.
const start = performance.now();
await executarOperacao();
const durationMs = performance.now() - start;
console.log({ durationMs });Para métricas financeiras, auditoria ou timestamps de negócio, use data e hora absolutas. Para duração, prefira relógio monotônico.
Marcas e medidas
Marcas identificam pontos; medidas calculam intervalos:
performance.mark('users:start');
const users = await repository.list();
performance.mark('users:database');
const result = users.map(normalizeUser);
performance.mark('users:end');
performance.measure(
'users:database-duration',
'users:start',
'users:database',
);
performance.measure(
'users:transform-duration',
'users:database',
'users:end',
);Essa abordagem facilita medir etapas sem manter várias variáveis de início.
PerformanceObserver
Um observador recebe entradas criadas:
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log({
name: entry.name,
entryType: entry.entryType,
startTime: entry.startTime,
duration: entry.duration,
});
}
});
observer.observe({ entryTypes: ['measure'] });Crie o observador antes das medidas que precisa capturar. Desconecte quando não for mais necessário:
observer.disconnect();Limpando entradas
A timeline ocupa memória. Em processos longos, limpe marcas e medidas:
performance.clearMarks('users:start');
performance.clearMarks('users:database');
performance.clearMarks('users:end');
performance.clearMeasures('users:database-duration');
performance.clearMeasures('users:transform-duration');Outra estratégia é consumir as entradas e limpar periodicamente. Não crie nomes únicos por requisição sem retenção controlada.
Timerify
performance.timerify() envolve uma função e gera entradas do tipo function:
function normalizeUser(user) {
return {
id: user.id,
name: user.name.trim(),
};
}
const timedNormalizeUser = performance.timerify(normalizeUser);
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(entry.name, entry.duration);
}
});
observer.observe({ entryTypes: ['function'] });
const normalized = timedNormalizeUser({ id: 1, name: ' Ana ' });O wrapper adiciona overhead. Use em diagnóstico, funções importantes ou amostragem, não automaticamente em todo o código.
Funções assíncronas
Timerify também pode medir uma função que devolve Promise:
const timedFetchUser = performance.timerify(async function fetchUser(id) {
return repository.findById(id);
});
await timedFetchUser(42);A duração inclui espera assíncrona. Para separar banco, rede e processamento local, crie spans ou medidas por etapa.
Histogramas
createHistogram() registra valores inteiros e calcula percentis:
const histogram = createHistogram();
for (let i = 0; i < 1000; i += 1) {
const start = process.hrtime.bigint();
await executarOperacao();
const durationNs = process.hrtime.bigint() - start;
histogram.record(durationNs);
}
console.log({
minNs: histogram.min,
maxNs: histogram.max,
meanNs: histogram.mean,
p95Ns: histogram.percentile(95),
p99Ns: histogram.percentile(99),
});Registre unidades consistentes. Histogramas desse módulo trabalham com inteiros; nanossegundos são úteis, mas converta para milissegundos na apresentação.
RecordableHistogram com timerify
Um histograma pode ser associado a uma função temporizada:
const histogram = createHistogram();
const timed = performance.timerify(executarOperacao, { histogram });
await timed();
console.log(histogram.percentile(99) / 1e6);Isso evita processar cada entrada manualmente e permite obter percentis locais.
Monitorando event loop delay
const delay = monitorEventLoopDelay({ resolution: 20 });
delay.enable();
setInterval(() => {
console.log({
minMs: delay.min / 1e6,
maxMs: delay.max / 1e6,
meanMs: delay.mean / 1e6,
p95Ms: delay.percentile(95) / 1e6,
p99Ms: delay.percentile(99) / 1e6,
});
delay.reset();
}, 10_000).unref();Quanto menor a resolução, maior o detalhe e potencialmente o custo. Meça overhead no ambiente real.
Event Loop Utilization
let previous = performance.eventLoopUtilization();
setInterval(() => {
const delta = performance.eventLoopUtilization(previous);
previous = performance.eventLoopUtilization();
console.log({ utilization: delta.utilization });
}, 10_000).unref();ELU mostra a proporção de atividade do event loop. Correlacione com delay, CPU, latência e throughput.
PerformanceResourceTiming
Aplicações e bibliotecas podem criar entradas de recursos para representar operações externas. Entretanto, em backends, traces distribuídos normalmente oferecem contexto mais completo. Use a timeline quando precisar de uma API local padronizada e de baixo acoplamento.
Medindo uma requisição HTTP
import { randomUUID } from 'node:crypto';
async function handleRequest(req, res) {
const id = randomUUID();
const startMark = `request:${id}:start`;
const endMark = `request:${id}:end`;
const measureName = `request:${id}`;
performance.mark(startMark);
try {
await route(req, res);
} finally {
performance.mark(endMark);
performance.measure(measureName, startMark, endMark);
performance.clearMarks(startMark);
performance.clearMarks(endMark);
performance.clearMeasures(measureName);
}
}O exemplo demonstra limpeza, mas nomes únicos podem gerar alta cardinalidade se forem exportados diretamente. Para métricas, agregue por rota normalizada, método e status, nunca por ID.
Integração com logs
Registre somente operações lentas:
async function measureSlowOperation(name, operation) {
const start = performance.now();
try {
return await operation();
} finally {
const durationMs = performance.now() - start;
if (durationMs > 250) {
logger.warn({ name, durationMs }, 'Operação lenta');
}
}
}Não registre payloads, tokens ou dados pessoais. Inclua IDs de correlação pequenos e controlados.
Integração com métricas
Converta medidas para histogramas do Prometheus ou OpenTelemetry:
const start = performance.now();
try {
return await operation();
} finally {
requestDuration.observe(
{ route: '/users/:id', method: 'GET' },
(performance.now() - start) / 1000,
);
}Confirme a unidade esperada pela biblioteca. Muitas métricas HTTP usam segundos.
PerformanceObserver e erros
O callback do observador deve ser simples. Trabalho pesado dentro dele pode perturbar a própria aplicação. Envie valores agregados para uma fila local ou atualize métricas em memória.
Benchmark de microfunções
perf_hooks ajuda a medir pequenos trechos, mas microbenchmarks exigem aquecimento, repetição e prevenção de otimizações irreais.
for (let i = 0; i < 10_000; i += 1) {
funcao();
}
const start = performance.now();
for (let i = 0; i < 1_000_000; i += 1) {
funcao();
}
console.log(performance.now() - start);Teste o comportamento completo sob carga antes de escolher uma implementação baseada apenas em nanossegundos locais.
Testes de regressão
Evite limites rígidos em runners compartilhados. Uma estratégia mais robusta é executar várias rodadas, usar mediana e bloquear somente regressões grandes.
if (medianMs > baselineMs * 1.25) {
throw new Error('Regressão de desempenho acima de 25%');
}Reserve benchmarks precisos para máquinas dedicadas e mantenha smoke tests no CI comum.
Overhead
Toda instrumentação possui custo. Marcas, observadores, histogramas, logs e exportadores podem aumentar CPU e memória. Meça com e sem instrumentação e use amostragem quando necessário.
Erros comuns
- usar
Date.now()para durações críticas; - não limpar marcas e medidas;
- criar nomes únicos exportados como labels;
- confundir milissegundos e nanossegundos;
- medir somente média;
- executar trabalho pesado no observador;
- usar microbenchmark como prova de capacidade real;
- ignorar aquecimento do V8;
- adicionar instrumentação sem medir overhead.
Fluxo recomendado
Use performance.now() para durações, marcas para etapas, histogramas para percentis e monitoramento do event loop para saúde do runtime. Agregue métricas por operações estáveis e valide sob carga. Combine com Event Loop Utilization, profiling em –prof, carga com Autocannon e observabilidade em Prometheus.
Consulte a documentação oficial de perf_hooks e a referência oficial de PerformanceObserver.




