Performance Profiling

Find the real bottleneck with perf_hooks, flame graphs, and event-loop lag — not guesses.

The first rule of performance work: measure before you optimize. Node ships enough tooling to find the actual hot path instead of guessing.

The tools, from quick to deep

  • perf_hooksperformance.now(), marks/measures, and PerformanceObserver for precise in-code timing.
  • --prof then --prof-process — the built-in V8 tick profiler; zero deps, produces a text breakdown.
  • Flame graphsnode --cpu-prof writes a .cpuprofile you open in Chrome DevTools or VS Code; the clinic.js suite (clinic flame, clinic doctor) makes this turnkey.
  • Event-loop lagmonitorEventLoopDelay() tells you when the loop is blocked, the single most useful production health metric for Node.
const { performance } = require('node:perf_hooks');
const t0 = performance.now();
expensive();
console.log(`took ${(performance.now() - t0).toFixed(1)}ms`);

Blocked loop = the usual culprit

Most Node latency problems are not slow I/O — they are a synchronous CPU spike (a big JSON.parse, a regex, a sync crypto call) blocking the loop so every in-flight request stalls. Event-loop delay percentiles catch this where average response time hides it.

Example

Example · javascript
// A lightweight, always-on performance harness: event-loop delay histogram
// plus GC observation. Ship something like this and you will SEE the spike.
const { monitorEventLoopDelay, PerformanceObserver, constants } = require('node:perf_hooks');

// 1) Event-loop lag histogram — the vital sign of a Node service.
const loop = monitorEventLoopDelay({ resolution: 10 });
loop.enable();

setInterval(() => {
  const ms = (ns) => (ns / 1e6).toFixed(1);
  console.log(`loop delay  p50=${ms(loop.percentile(50))}ms  p99=${ms(loop.percentile(99))}ms  max=${ms(loop.max)}ms`);
  loop.reset();
}, 5000).unref();

// 2) Observe GC pauses — long ones correlate with latency cliffs.
const gcObserver = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.duration > 50) {
      console.warn(`long GC: ${entry.duration.toFixed(1)}ms kind=${entry.detail?.kind}`);
    }
  }
});
gcObserver.observe({ entryTypes: ['gc'] });

// Simulate a blocking spike to see p99 lag jump in the next report.
setTimeout(() => {
  const end = Date.now() + 300;
  while (Date.now() < end) {} // synchronous stall: DON'T do this in real code
}, 2000);

When to use it

  • An SRE uses `clinic.js flame` to generate a flame graph of a Node.js API under load and identifies a hot path in JSON serialization taking 40% of CPU.
  • A developer uses `node --prof` and `node --prof-process` to find that a regular-expression in a middleware is O(n^2) on long strings.
  • A memory-leak investigation uses `heapdump` to capture two snapshots -- one before and one after a leak -- and diffs them in Chrome DevTools to find the growing object.

More examples

CPU profiling with --prof

Uses Node's built-in V8 profiler to collect CPU ticks and `--prof-process` to find the hottest functions.

Example · bash
# 1. Run app with V8 profiler
node --prof server.js
# (send traffic, then Ctrl+C)

# 2. Process the generated isolate-*.log
node --prof-process isolate-*.log > profile.txt

# 3. Look for high-tick functions
grep -A5 '\[Bottom up\]' profile.txt | head -20

Heap snapshot for memory leak

Uses `v8.writeHeapSnapshot()` to dump the current heap to a file that can be analyzed in Chrome DevTools.

Example · js
const v8 = require('v8');
const fs = require('fs');

// Take a heap snapshot and write it to disk
const snap = v8.writeHeapSnapshot();
console.log('Snapshot written:', snap);
// Open the .heapsnapshot file in Chrome DevTools -> Memory

Measure async event-loop lag

Monitors event-loop delay with a histogram and logs min/mean/p99 latency every second to detect CPU-blocking code.

Example · js
const { monitorEventLoopDelay } = require('perf_hooks');

const histogram = monitorEventLoopDelay({ resolution: 10 });
histogram.enable();

setInterval(() => {
  console.log({
    min:  (histogram.min  / 1e6).toFixed(2) + 'ms',
    mean: (histogram.mean / 1e6).toFixed(2) + 'ms',
    p99:  (histogram.percentile(99) / 1e6).toFixed(2) + 'ms',
  });
  histogram.reset();
}, 1000);

Discussion

  • Be the first to comment on this lesson.