Performance & perf_hooks
Why measure?
Section titled “Why measure?”“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()
Section titled “performance.now()”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 — async collection
Section titled “PerformanceObserver — async collection”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 leaksobs.disconnect();Live demo — timing a CPU-bound loop
Section titled “Live demo — timing a CPU-bound loop”Needs the Node.js runtime — open in StackBlitz to run.
Keeping the event loop unblocked
Section titled “Keeping the event loop unblocked”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 durationfunction 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 setImmediateasync 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 Threadimport { Worker } from 'node:worker_threads';// (see the Streams & Worker Threads module for full coverage)CPU profiling with --prof
Section titled “CPU profiling with --prof”For deeper CPU profiling, Node ships a built-in V8 profiler:
# Run with profiling enablednode --prof src/app.js
# Process the isolate log into a readable reportnode --prof-process isolate-*.log > profile.txtThe output shows which functions consumed the most CPU time (ticks). Look for hot paths in the “Bottom up (heavy) profile” section.