Measure Before You Optimize

Ask five developers which line of a slow handler to fix and you will get five answers, most of them wrong. The search endpoint below reads a 480 KB product catalog from disk, parses it, filters 4,000 rows and serializes twenty results; the obvious suspect is the filter, the only line that looks like work. performance.mark() and performance.measure() from node:perf_hooks settle the argument in twenty lines, and unlike a profiler they cost almost nothing to leave in.

Timing the phases of one handler with performance marksJavaScript
import { performance as p, PerformanceObserver } from 'node:perf_hooks';
const total = new Map();
new PerformanceObserver((l) => {
  for (const e of l.getEntries()) total.set(e.name, (total.get(e.name) ?? 0) + e.duration);
}).observe({ type: 'measure' });
const phase = (name, fn) => { p.mark('s'); const v = fn(); p.measure(name, 's'); return v; };
function handle(category) {
  const text = phase('read file', () => readFileSync('catalog.json', 'utf8'));
  const rows = phase('JSON.parse', () => JSON.parse(text));
  const hit = phase('filter', () => rows.filter((r) => r.category === category));
  return phase('stringify', () => JSON.stringify({ count: hit.length, items: hit.slice(0, 20) }));
}
for (let i = 0; i < 200; i++) handle('alpha');
setImmediate(() => {
  const all = [...total.values()].reduce((a, b) => a + b, 0);
  for (const [n, ms] of [...total].sort((a, b) => b[1] - a[1]))
    console.log(`${n.padEnd(11)} ${(ms / 200).toFixed(2).padStart(6)} ms/request ` +
      `${(100 * ms / all).toFixed(1).padStart(5)}%`);
});
Output
JSON.parse    2.78 ms/request  71.3%
read file     1.03 ms/request  26.4%
filter        0.08 ms/request   2.0%
stringify     0.01 ms/request   0.3%

The filter is 2% of the request. Rewriting it as a hand-tuned for loop — the change most people reach for first — would at best save eight hundredths of a millisecond. The parse is 71%, and the fix is not faster parsing but no parsing: read the catalog once at startup.

Three rules follow. Measure the real workload, not a loop you invented. Measure a percentile, not an average — Load Testing an HTTP Server shows a server whose mean looks survivable while its 99th percentile is a quarter of a second. And measure again afterward, because a good share of optimizations make things slower. Marks are cheap but not free, so guard the instrumentation with an environment variable and call performance.clearMarks() periodically. When marks are not enough, Debugging and Profiling takes over: flame graphs attribute time across a process, heap snapshots find retained memory, and the event-loop delay histogram proves whether a handler is blocking.