Performance Hooks (perf_hooks)
The node:perf_hooks module exposes the Web Performance API (the same performance.now(), performance.mark(), performance.measure() you use in browsers) plus Node-specific APIs: high-resolution timing, GC observation, event loop delay monitoring, and HTTP/DNS/connection timings. It's the standard way to instrument code for performance analysis without reaching for external benchmarking libraries.
High-resolution timing with performance.now()
import { performance } from 'node:perf_hooks'
// performance.now() returns milliseconds with sub-millisecond precision (microseconds):
const start = performance.now()
await someOperation()
const duration = performance.now() - start
console.log(`Operation took ${duration.toFixed(3)}ms`)
// Compared to Date.now() which has 1ms resolution and can skew with clock adjustments:
// Date.now() → integer milliseconds, affected by NTP/clock changes
// performance.now() → float milliseconds from process start, monotonically increasingUser timing: mark and measure
import { performance, PerformanceObserver } from 'node:perf_hooks'
// Mark named points in time:
performance.mark('db-query-start')
await db.query('SELECT ...')
performance.mark('db-query-end')
// Measure the interval between marks:
performance.measure('db-query', 'db-query-start', 'db-query-end')
// Read the measure:
const [measure] = performance.getEntriesByName('db-query', 'measure')
console.log(`DB query: ${measure.duration.toFixed(2)}ms`)
// Or observe all marks/measures via PerformanceObserver (non-blocking):
const obs = new PerformanceObserver((items) => {
for (const entry of items.getEntries()) {
console.log(`${entry.name}: ${entry.duration.toFixed(2)}ms`)
}
performance.clearMarks()
performance.clearMeasures()
})
obs.observe({ entryTypes: ['measure', 'mark'] })Observing GC events
import { PerformanceObserver, constants } from 'node:perf_hooks'
const gcObs = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
const detail = entry.detail as { kind: number; flags: number }
const type = detail.kind === constants.NODE_PERFORMANCE_GC_MAJOR
? 'major' : detail.kind === constants.NODE_PERFORMANCE_GC_MINOR
? 'minor' : 'incremental'
console.log(`GC [${type}] ${entry.duration.toFixed(2)}ms`)
}
})
gcObs.observe({ entryTypes: ['gc'] })GC [minor] 0.84ms GC [minor] 1.02ms GC [major] 12.37ms
Event loop delay monitoring
import { monitorEventLoopDelay } from 'node:perf_hooks'
// Histogram of event loop delay sampled every 10ms:
const histogram = monitorEventLoopDelay({ resolution: 10 })
histogram.enable()
// Report percentiles periodically:
setInterval(() => {
const stats = {
min: (histogram.min / 1e6).toFixed(2) + 'ms',
mean: (histogram.mean / 1e6).toFixed(2) + 'ms',
p50: (histogram.percentile(50) / 1e6).toFixed(2) + 'ms',
p95: (histogram.percentile(95) / 1e6).toFixed(2) + 'ms',
p99: (histogram.percentile(99) / 1e6).toFixed(2) + 'ms',
max: (histogram.max / 1e6).toFixed(2) + 'ms',
}
console.log('Event loop delay:', stats)
histogram.reset()
}, 10_000)
// Values are in nanoseconds — divide by 1e6 for milliseconds.
// p99 > 10ms consistently indicates event loop blocking that needs investigation.HTTP timing details (Node's built-in)
import { performance, PerformanceObserver } from 'node:perf_hooks'
import http from 'node:http'
// Observe HTTP request timing details (DNS lookup, TCP connect, TLS, TTFB, etc.):
const obs = new PerformanceObserver((items) => {
for (const entry of items.getEntries()) {
const detail = entry.detail as any
console.log({
url: entry.name,
total: entry.duration.toFixed(2) + 'ms',
dns: detail.dnsLookupAt ? (detail.dnsLookupAt - entry.startTime).toFixed(2) + 'ms' : 'n/a',
connect: detail.connectAt ? (detail.connectAt - entry.startTime).toFixed(2) + 'ms' : 'n/a',
firstByte: detail.firstByteAt ? (detail.firstByteAt - entry.startTime).toFixed(2) + 'ms' : 'n/a',
})
}
})
obs.observe({ entryTypes: ['http'] })
// Make a request — timing is captured automatically:
http.get('http://example.com', (res) => res.resume())Benchmarking with timerify
import { performance, PerformanceObserver } from 'node:perf_hooks'
function sortArray(arr: number[]): number[] {
return [...arr].sort((a, b) => a - b)
}
// Wrap a function to automatically record its execution time on each call:
const timedSort = performance.timerify(sortArray)
const obs = new PerformanceObserver((items) => {
for (const entry of items.getEntries()) {
console.log(`${entry.name}: ${entry.duration.toFixed(3)}ms`)
}
obs.disconnect()
})
obs.observe({ entryTypes: ['function'], buffered: true })
// Each call to timedSort records an entry:
const data = Array.from({ length: 10_000 }, () => Math.random())
timedSort(data)
timedSort(data)sortArray: 2.145ms sortArray: 1.832ms
perf_hooks vs external benchmarking
Tool | Best for |
|---|---|
| Quick inline timing of a code section |
| Multi-point profiling with named spans; DevTools compatible |
| Continuous production monitoring of event loop health |
| GC pause monitoring in production |
| Benchmarking a specific function across many calls |
| CPU profiling with V8 tick sampling; finds hot functions |
| Comprehensive profiling (flame graphs, event loop, memory) for production-like workloads |