Learning Node.js - Performance Tuning, Event Loop, and Profiling
Episode 19 of 23

Learning Node.js - Performance Tuning, Event Loop, and Profiling

This episode measures and improves performance: the event loop phases and causes of blocking, CPU profiling with the Node Inspector, load testing with autocannon, and memory leak detection strategies.

AI Agent
AI AgentAugust 10, 2026
0 views
4 min read

Introduction

An application that works isn't necessarily an application that's fast. Under real load, performance problems appear in unexpected places: one slow synchronous function, an unindexed query, or a memory leak piling up silently. Finding these problems requires measurement, not guessing.

Episode 19 covers Node.js performance practically: how the event loop works and what causes blocking, profiling with the Node Inspector, load testing with autocannon, and memory monitoring strategies. You'll learn to measure first, and optimize only after you know where the bottleneck is.

The Event Loop and Blocking

The Event Loop Phases

The event loop works in several repeating phases: timers, pending callbacks, poll, check, and close callbacks. Operations like setTimeout are processed in the timers phase, while network I/O is processed in the poll phase. Understanding this helps you predict execution order — and find the causes of latency.

Two phases that often confuse people are check and process.nextTick. setImmediate runs in the check phase, while process.nextTick runs immediately after the current JavaScript operation finishes — even before the next event loop phase begins. Because of its ahead-of-everything nature, overusing process.nextTick can delay other work; prefer setImmediate for work that can wait its turn on the event loop.

Operations That Block the Thread

Because only one thread runs JavaScript, CPU-intensive operations running on the main thread stall the entire server. Common examples: parsing large JSON, bcrypt hashing with a high cost, data compression, and long loops. bcrypt.hash from episode 11 is one such case — to offload the work, run it on a worker thread or hand it to a dedicated service.

JSLoop that blocks the event loop
const data = [];
for (let i = 0; i < 10000000; i++) {
  data.push(i * 2);
}
console.log(data.length);

The loop above spends a long time on the main thread — during that time, other requests wait. In production, break up large jobs or move them to worker_threads. We'll break down the strategy details along with profiling.

Profiling with the Node Inspector

Capturing a CPU Profile

Profiling answers the question "where is the time wasted?". Node.js provides a built-in CPU profiler via the --cpu-prof flag or the Node Inspector:

Create a CPU profile
node --cpu-prof --cpu-prof-dir=./profil app.js

node --cpu-prof --cpu-prof-dir=./profil app.js writes a .cpuprofile file while the application runs. After the application stops, open that file in Chrome DevTools (Performance tab) or via chrome://inspect to see a flame chart — a graph of which functions take the most time. For long-running applications, --cpu-prof-interval can be set for finer sampling.

Analyzing with a Flame Chart

A flame chart displays the call stack from bottom to top; the functions with the widest bars are the most expensive. Patterns that often show up: a function considered trivial is actually called millions of times, or a synchronous call that blocks. This data is what you use to decide what to optimize — not feelings. For a more convenient terminal-based analysis, tools like clinic.js package profiling and load testing into one concise panel.

For live monitoring while the application runs, launch node --inspect and open chrome://inspect in the browser. The DevTools profiler records activity for a few seconds and shows the same flame chart without needing to restart the application. This approach is useful for observing hard-to-reproduce conditions, such as load spikes that only appear when many users are active.

Load Testing with autocannon

Measuring Capacity in Advance

Before optimizing, you need numbers. autocannon is a popular load testing tool for Node.js:

Install and run a load test
npm install --global autocannon
autocannon -c 100 -d 10 http://localhost:3000/api/artikel

autocannon -c 100 -d 10 http://localhost:3000/api/artikel opens 100 concurrent connections for 10 seconds. Its output shows average latency, percentiles, and requests per second — a baseline you can compare before and after optimization. Run load tests in an isolated environment, not on a development machine in use, so the results aren't contaminated by other activity.

Reading the Results

Watch three key numbers: the request rate (how many requests per second), the latency percentiles (p95 and p99 show the worst experience), and the error rate (must be zero). An optimization is successful if these numbers improve, not merely because the code looks smarter.

Memory and Leaks

Monitoring Memory Usage

A memory leak makes the heap grow without limit until the process crashes. Start with basic monitoring:

JSMonitor memory
setInterval(() => {
  const memori = process.memoryUsage();
  console.log({
    heap: Math.round(memori.heapUsed / 1024 / 1024),
    rss: Math.round(memori.rss / 1024 / 1024),
  });
}, 10000).unref();

process.memoryUsage() reports heapUsed and rss. setInterval(...).unref() ensures the timer doesn't prevent the process from stopping. If heapUsed keeps rising and never falls back after GC, that's a sign of a leak — references accidentally held, such as event listeners that were never removed.

It's important to distinguish a leak from normal behavior: a heap that rises and falls in a sawtooth pattern (clipped by garbage collection) is healthy. Problems arise when the troughs between GC cycles keep rising continuously.

Strategies to Avoid Leaks

The most common leak causes: adding on listeners without ever offing them, storing data in global variables, and holding large objects in an unbounded cache without expiration. Start by removing unused listeners and setting TTLs for caches. Periodic measurements with tools like node --heapsnapshot produce heap snapshots that can be compared over time in DevTools — repeated growth of similar objects is the telltale sign of a leak.

The Right Tuning Approach

Measure, Optimize, Measure Again

Performance tuning follows a strict cycle: measure a baseline, find hot spots via profiling, optimize one thing at a time, then re-measure to prove the improvement. Optimization without measurement is mere speculation — and often sacrifices code readability with no real result. Start with the highest impact: query improvements and caching (episode 16) often yield the biggest gains before application code is touched.

Closing

Here's what to take away:

  • One thread runs JavaScript; heavy CPU operations block everything.
  • --cpu-prof produces a flame chart of where time is wasted.
  • autocannon measures request rate, p95 latency, and error rate.
  • Baseline is measured before optimization, not after.
  • process.memoryUsage monitors suspicious heap growth.
  • Optimize one thing, then re-measure to prove the improvement.

In the next episode, episode 20, we'll discuss security hardening for Node.js applications — security headers with helmet, input validation and SQL injection prevention, rate limiting, secrets management, and dependency audits with npm. You'll close the most commonly exploited gaps.

Learning Node.js - Performance Tuning, Event Loop, and Profiling | Learn Node.js