Skip to content

Performance & perf_hooks

“Premature optimization is the root of all evil.” — Donald Knuth

Before you optimize anything, measure first. Node’s perf_hooks module gives you sub-millisecond timing via the High Resolution Time API — the same performance.now() you know from browsers, but available natively in Node without any import in newer versions (global since Node 16), and as perf_hooks for PerformanceObserver.

performance.now() returns a float in milliseconds with microsecond precision. It is monotonic — it never goes backwards, even across system clock adjustments:

import { performance } from 'node:perf_hooks';
const start = performance.now();
// --- work to measure ---
let sum = 0;
for (let i = 0; i < 1_000_000; i++) {
sum += i;
}
// ----------------------
const elapsed = performance.now() - start;
console.log('Sum loop took: ' + elapsed.toFixed(3) + ' ms');

performance.mark() and performance.measure()

Section titled “performance.mark() and performance.measure()”

For named measurements that show up in DevTools timelines:

import { performance, PerformanceObserver } from 'node:perf_hooks';
performance.mark('db-start');
// ... simulate a DB query ...
await new Promise(function(resolve) { setTimeout(resolve, 50); });
performance.mark('db-end');
performance.measure('database-query', 'db-start', 'db-end');
const entries = performance.getEntriesByName('database-query');
console.log(entries[0].duration.toFixed(2) + ' ms');

PerformanceObserver fires a callback whenever a new performance entry is created. This is useful for instrumenting library code that you do not control:

import { PerformanceObserver, performance } from 'node:perf_hooks';
const obs = new PerformanceObserver(function(list) {
for (const entry of list.getEntries()) {
console.log(entry.name + ': ' + entry.duration.toFixed(2) + ' ms');
}
});
obs.observe({ entryTypes: ['measure'] });
performance.mark('a');
// ... work ...
performance.mark('b');
performance.measure('my-operation', 'a', 'b');
// Always disconnect when done to avoid memory leaks
obs.disconnect();
Node.js

Needs the Node.js runtime — open in StackBlitz to run.

A CPU-bound loop that runs for more than ~50 ms will block the event loop — no I/O, no timers, no other requests can be processed while it runs.

// BAD — blocks the event loop for the entire duration
function blockingWork(data) {
const result = [];
for (const item of data) {
result.push(expensiveTransform(item)); // each call takes ~1ms, 10000 items = 10s block
}
return result;
}
// BETTER — yield control periodically with setImmediate
async function nonBlockingWork(data) {
const result = [];
for (let i = 0; i < data.length; i++) {
result.push(expensiveTransform(data[i]));
if (i % 100 === 0) {
// Yield to the event loop every 100 items
await new Promise(function(resolve) { setImmediate(resolve); });
}
}
return result;
}
// BEST for CPU-heavy work — offload to a Worker Thread
import { Worker } from 'node:worker_threads';
// (see the Streams & Worker Threads module for full coverage)

For deeper CPU profiling, Node ships a built-in V8 profiler:

Terminal window
# Run with profiling enabled
node --prof src/app.js
# Process the isolate log into a readable report
node --prof-process isolate-*.log > profile.txt

The output shows which functions consumed the most CPU time (ticks). Look for hot paths in the “Bottom up (heavy) profile” section.

What unit does `performance.now()` return?
Why is `performance.now()` preferred over `Date.now()` for benchmarking?
What problem does a long-running CPU-bound loop cause in Node.js?
Which technique yields control back to the event loop periodically during a large loop?