Clinic.js is a collection of tools that help you understand and improve the performance of your Node.js applications.
Let’s see clinic.js in action. Imagine you have a Node.js application that’s experiencing performance issues. You suspect a CPU bottleneck, but you’re not sure where to look. You can use clinic doctor to get a general overview of your application’s health and then clinic profiler to dive deep into CPU usage.
First, install clinic.js:
npm install -g clinic
Now, let’s run your Node.js application with clinic doctor. Replace your-app.js with the actual name of your entry point file.
clinic doctor -- node your-app.js
clinic doctor will start your application and collect various metrics like CPU usage, event loop latency, and memory consumption. Once your application has run for a bit (or you’ve triggered the performance issue), press Ctrl+C to stop the collection. Clinic will then generate an HTML report. Open this report in your browser.
The clinic doctor report gives you a high-level view. You’ll see charts for CPU utilization, event loop delay, and memory usage over time. If you see consistently high CPU usage, or spikes in event loop delay, it’s a good indication that you need to investigate further.
To pinpoint the exact functions consuming CPU, we use clinic profiler. Stop clinic doctor if it’s still running and run your application with clinic profiler:
clinic profiler -- node your-app.js
Again, let your application run and trigger the performance issue. Press Ctrl+C to stop. clinic profiler will generate a different HTML report, this time focused on CPU profiling.
This clinic profiler report is where the magic happens for finding hotspots. You’ll see a flame graph. In a flame graph, the width of each "flame" (or bar) represents the amount of CPU time spent in a particular function. Wider bars mean more CPU time. The height represents the call stack depth.
To interpret the flame graph:
- Wide bars at the bottom: These indicate functions that are doing a lot of work themselves.
- Wide bars higher up: These indicate that a function is calling other functions that consume a lot of CPU time. The function at the top of a wide stack is the one that’s initiating the expensive operation.
- Look for distinct, wide columns: These are your prime suspects for performance bottlenecks.
Clicking on a flame in the graph will zoom in, showing you the functions that were called from within it. This helps you drill down into the call stack and understand the context of the CPU-intensive work.
The goal is to identify functions that are unexpectedly wide, meaning they are consuming a disproportionate amount of CPU time. Often, these are not the functions you might intuitively suspect. For example, you might find that a seemingly innocuous data transformation or serialization routine is actually the culprit.
The problem this solves is the common difficulty of identifying where in your Node.js code CPU cycles are being spent. Without tools like clinic profiler, you’re often left guessing or resorting to less precise console.log timing, which can itself alter performance characteristics. Clinic provides a clear, visual representation of CPU usage that directly maps to your code.
Internally, clinic profiler uses Node.js’s built-in V8 profiler. It starts the profiler when your application launches, periodically samples the call stack, and then processes this data into the flame graph format. The sampling mechanism is what allows it to be relatively low-overhead compared to some other profiling techniques, but it’s important to remember it’s a snapshot in time and might miss very short-lived, but frequent, bursts of activity.
The exact levers you control are mainly the application you run and the duration for which you run it. Longer runs can capture more representative profiles, especially for intermittent issues. You can also use clinic profiler --collect-only to generate raw profiling data, which can then be analyzed later with clinic codegen if needed, offering more flexibility.
One thing most people don’t know is that the flame graph is interactive not just for zooming, but also for filtering. If you know a specific module or function name is suspect, you can often type it into a search box within the report to highlight relevant parts of the graph. This is incredibly useful for narrowing down the search space when your application has a very large codebase.
After you’ve identified and optimized your CPU hotspots, you might then want to investigate how your application handles network requests or I/O operations.