The single most expensive mistake in performance work isn't a bad optimization — it's a correct optimization applied to code that was never the bottleneck. An engineer spends three days making a function twice as fast, ships it, and total request latency doesn't move, because that function was responsible for four percent of the time spent. Profiling before optimizing isn't a nice-to-have discipline; it's the only thing that tells you whether the three days were worth spending at all.

Start with a number, not a hunch

"This feels slow" is a starting point for investigation, not a target for optimization. Before touching any code, get a baseline measurement of the actual thing users experience — page load time, API response time at a specific percentile, job completion time — using the same conditions you'll measure against after the change. Without this, you have no way to tell a real improvement from noise, and noise in performance measurement is larger than most people expect: network jitter, cache state, and background load can easily swing a measurement by twenty percent run to run.

Pick a percentile that matches what you're trying to fix. Median latency and p99 latency are frequently dominated by completely different causes — median is usually about typical-path efficiency, while p99 is usually about a specific slow path, a lock contention pattern, or GC pauses that only bite occasionally. Optimizing for the wrong percentile is a quieter version of the same mistake as optimizing the wrong function.

Profile before guessing at the cause

Once you know something is actually slow, a profiler tells you where the time goes instead of where you assume it goes. CPU profilers (flamegraphs are the most useful visualization for this) show which functions consume wall-clock or CPU time; they're the right tool when a process is pegging a core. They're the wrong tool when the process is mostly idle and waiting — for a database, a downstream service, or a lock — which is a much more common cause of latency in web backends than raw CPU cost.

bash
# Sampling a running Node process without restarting it
node --prof app.js
node --prof-process isolate-*.log > profile.txt

For I/O-bound latency, distributed tracing is a better lens than a CPU profiler — it shows you the waterfall of calls a single request makes, which reveals problems a profiler on any one service would miss entirely: an N+1 query pattern spread across a loop, a serial chain of calls that could run in parallel, or a downstream service that's slow for reasons your own code has no visibility into.

Change one thing, remeasure, repeat

The instinct after finding a bottleneck is often to fix everything the profile surfaced in one pass. Resist it. Change one thing, remeasure against the same baseline conditions, and confirm the change actually moved the number before making the next change. This catches two failure modes: optimizations that don't help as much as they looked like they would on paper, and optimizations that help in isolation but interact badly with something else you changed in the same pass.

It also protects against the most common trap in profiling work — over-indexing on whatever the flamegraph makes visually obvious. A wide bar in a flamegraph means a function took a lot of cumulative time, not necessarily that reducing it is easy, safe, or worth the engineering cost relative to a narrower bar elsewhere that happens to sit on a much simpler fix. Profiling tells you where the time is. It's still your judgment that decides which of those places is worth spending an afternoon on.