Performance Overview
Node.js is fast by default for the workload it was designed for — I/O-bound servers handling many concurrent connections — but it's easy to accidentally make it slow. Because everything runs on a single thread, one blocking operation degrades every request at once, so Node performance work is mostly about not blocking the event loop and not doing redundant work. This page frames the discipline: measure before optimizing, understand the I/O-bound vs CPU-bound divide, know the common bottlenecks, and learn the order in which to attack them — before later pages dig into profiling, memory leaks, caching, and more.
Measure first — the cardinal rule
I/O-bound vs CPU-bound — they need opposite fixes
I/O-bound | CPU-bound | |
|---|---|---|
Time spent | Waiting on DB, network, disk | Computing — crypto, parsing, image work |
Node default fit | Excellent — async I/O, high concurrency | Poor — blocks the single thread |
The fix | Parallelize, cache, batch, add indexes | Offload: worker threads, child processes, native code |
Symptom | Slow but loop responsive | Event-loop lag, all requests stall together |
The usual suspects, roughly in order of impact
Database — N+1 queries, missing indexes, fetching too much, no connection pooling. Usually the #1 cost in a web app.
Blocking the event loop — synchronous crypto/
fs, huge JSON, regex backtracking; stalls every request.No caching — recomputing or re-fetching identical data on every request (caching strategies).
Serial instead of parallel —
awaiting independent operations one-by-one instead ofPromise.all.Oversized payloads — sending more data than the client needs; no compression or pagination.
Memory pressure — leaks and excess allocation triggering frequent GC pauses.
The most common quick win: parallelize independent awaits
// ❌ Serial — 3 round-trips back to back (≈ sum of all three): const user = await getUser(id) const orders = await getOrders(id) const prefs = await getPrefs(id) // ✅ Parallel — they don't depend on each other, so fire together (≈ the slowest one): const [user, orders, prefs] = await Promise.all([ getUser(id), getOrders(id), getPrefs(id), ])
An optimization workflow
Set a goal — a concrete target (p95 < 200ms, 1000 req/s) so you know when to stop.
Establish a baseline — measure current behaviour under realistic load.
Find the bottleneck — profile CPU and inspect slow queries; let data point you.
Fix the biggest one — address the dominant cost, not a 1% sliver.
Measure again — confirm the change actually helped and didn't regress correctness.
Repeat or stop — iterate until you hit the goal; resist optimizing past "good enough".