Concept
The beginner framing: a Node.js server that feels sluggish or that gradually consumes more and more memory over time needs to be measured, not guessed at, profiling tools show exactly where CPU time and memory are actually going.
The precise mental model: performance work in Node.js splits into two distinct problems that need different tools, CPU profiling (where is the single main thread spending its time?) and memory profiling (what's being retained in memory, and why isn't it being garbage collected?).
CPU profiling
node --cpu-prof server.js
# generates a CPU-<pid>-<timestamp>.cpuprofile file--cpu-prof uses V8's built-in sampling profiler to record where the call stack spends its time, writing a .cpuprofile file that can be loaded directly into Chrome DevTools' Performance panel for a flame-graph view, showing exactly which functions consumed the most main-thread time. This directly builds on the single-main-thread model from Runtime Overview: CPU profiling exists specifically because that one thread is the shared, finite resource every request competes for.
node --prof server.js
# generates isolate-<pid>-v8.log, needs post-processing:
node --prof-process isolate-0x*-v8.log > processed.txtThe older --prof flag is V8's built-in profiler in its raw form, it requires an extra post-processing step (--prof-process) to turn the raw log into readable output, whereas --cpu-prof directly produces a file ready for DevTools.
Memory leaks: the common real-world cause
// A cache that grows forever, with no eviction:
const cache = new Map();
app.get("/data/:id", (req, res) => {
if (!cache.has(req.params.id)) {
cache.set(req.params.id, expensiveComputation(req.params.id));
}
res.json(cache.get(req.params.id));
});
// every DISTINCT id ever requested stays in memory FOREVERThe most common real-world memory leak isn't exotic, it's an unbounded cache (a Map/object that keeps growing with no eviction policy) or event listeners that are attached repeatedly and never removed (the exact pattern covered in EventEmitter & Async Patterns's MaxListenersExceededWarning). Both share the same root cause: something is holding a live reference to data that should have been eligible for garbage collection.
node --inspect server.js
# then open chrome://inspect in Chrome, take heap snapshots
# over time, compare snapshots to see what's GROWING and never shrinkingTaking two heap snapshots at different points in time (via --inspect + Chrome DevTools' Memory panel) and comparing them reveals exactly which object types are accumulating, the comparison view highlights objects present in the second snapshot but not garbage-collected since the first, which is usually the fastest way to actually locate a leak's source.
Modern built-in tooling: node:test and --watch
node --test # runs all *.test.js files, STABLE, no external framework needed
node --watch server.js # restarts on file change, STABLE, no nodemon needed