# Profiling Node.js application with VS Code

Profiling your Node.js applications could be exhausting, especially when you have to switch between different tools to get a full picture of your app's performance.

The constant switching of contexts can kill your productivity.

What if I tell you that it doesn't have to be that way? What if you could perform all the necessary profiling routine within the same workspace you're already using for coding?

In this article, we'll explore how to use the VS Code built-in debugger to profile and troubleshoot common performance issues in your Node.js application.

You'll be surprised how much you can do in terms of profiling by just using VS Code.

## Setup

To illustrate the profiling process, we'll need some code. I've created a [GitHub repository](https://github.com/pavel-romanov8/nodejs-profiling-examples) that contains common performance issues you might encounter in your Node.js application.

The repository contains a simple Node.js application with three routes, each designed to demonstrate a specific performance issue.

* A CPU-intensive task that blocks the main thread.
    
* An asynchronous operation with the waterfall problem, where execution goes sequentially instead of in parallel.
    
* A memory leak.
    

Each route has two implementations: one with the problem that can be spotted using the VS Code profiler, and the other is an optimized version offering with the same functionality.

I encourage you to clone the repository and explore the code to better understand the topics we're about to discuss.

## Profiling

Now that we've set up the project let's explore how profiling in VS Code works.

Before diving into the specific problems, I want to mention that VS Code generates a profiling report after each profiling session. This profiling report can be viewed in 2 different ways:

* Table
    
* Flamegraph
    

While the table view is built-in, the flamegraph view requires a [separate flamegrap extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode.vscode-js-profile-flame) to enable it.

Having multiple ways to visualize your data leads to better understanding. You can catch insights using one type of view that are hard to notice using the other type.

### CPU-intensive endpoint

We start with the CPU-intensive endpoint. The main problem behind every CPU-intensive operation in JavaScript is that it blocks the execution thread. Other tasks can hardly make any progress while a CPU-intensive operation is running.

Sure, it can be solved by moving this task into a dedicated thread, but more often than not, you can avoid it by using a more efficient algorithm or data structure.

In our case, there are two implementations of this endpoint: one with a high CPU load and the other without it.

Let's look at implementation with high CPU consumption first.

```javascript
function runCpuIntensiveTask(cb) {
  function fibonacciRecursive(n) {
    if (n <= 1) {
      return n;
    }
    return fibonacciRecursive(n - 1) + fibonacciRecursive(n - 2);
   }
  fibonacciRecursive(45);
  cb();
}
```

Here is the code that does the same thing in terms of functionality but consumes way less CPU resources.

```javascript
function runSmartCpuIntensiveTask(cb) {
  function fibonacciIterative(n) {
    if (n <= 1) {
      return n;
	}
	let prev = 0, curr = 1;
	for (let i = 2; i <= n; i++) {
	  const next = prev + curr;
	  prev = curr;
	  curr = next;
	}
    return curr;
  }
  fibonacciIterative(45)
  cb();
}
```

Both versions calculate the 45th Fibonacci number. The first implementation uses a recursive, CPU-intensive approach, while the second one employs an iterative, more efficient approach.

To start the profiling session in VS Code, you should follow these steps:

1. Open the Debugger tab, typically located on the left panel.
    

![Debugger view](https://cdn.hashnode.com/res/hashnode/image/upload/v1727879834487/20f2019a-7a32-4259-a787-643b083cad90.png align="center")

2. Choose the script you want to execute in the "Run and Debug" section.
    
    ![Example of how to select debugger runner](https://cdn.hashnode.com/res/hashnode/image/upload/v1727879869222/5fae0e0e-8606-427f-85b7-06ac85688eae.png align="center")
    
3. Choose the appropriate profiling option. For CPU-intensive endpoint, we select "CPU profile".
    
    ![Take performance profile button in the call stack section](https://cdn.hashnode.com/res/hashnode/image/upload/v1727879896672/5f618c15-7808-465f-8400-9b5ef422d84d.png align="center")
    
4. Navigate to the "Call stack" section and click the "Take performance profile" button.
    
    ![Profiling option in VS Code debugger session](https://cdn.hashnode.com/res/hashnode/image/upload/v1727879990734/b2b26a51-2441-44e3-afe9-0d2f8164965f.png align="center")
    
5. Choose the run option. For simplicity, we'll use the "Manual" option.
    
    ![VS Code profiler run options](https://cdn.hashnode.com/res/hashnode/image/upload/v1727880082944/417eb16c-3ff6-4502-b359-592a582ac344.png align="center")
    

After going through all these steps, we're ready to start profiling.

Since the Node.js server is already running we only have to send a request to the CPU-intensive endpoint. We'll start with the implementation that consumes a lot of CPU resources and see if we can identify the problem just by looking at the profiling report.

Here are the profiler entries after sending the request:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1727880159629/45097704-053f-411f-9508-46259db88e33.png align="center")

As you can see, it is pretty easy to identify the bottleneck, it is the fibonacci function.

The results are presented in the table view. If you prefer a visual representation, you can switch to the flamegraph by clicking the flame button.

![VS Code profiler flamegraph button](https://cdn.hashnode.com/res/hashnode/image/upload/v1727880600909/0417e880-6060-4c8a-9ae4-dca9bd52a03b.png align="center")

Remember, this button is only available after installing the [flamegrap extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode.vscode-js-profile-flame).

After clicking this button, you will see a picture similar to the timeline and entries.

![VS Code profiler flamegraph view](https://cdn.hashnode.com/res/hashnode/image/upload/v1727880674750/4878885c-7a2e-4bae-8e17-cf6a9567e9e4.png align="center")

Now that we've identified the problem, let's replace the recursive implementation with an iterative one and profile the improved version of the CPU-intensive endpoint.

After sending the request to the endpoint with the improved fibonacci function we see the following results:

![Profiling results of improved CPU-intensive endpoint in table view](https://cdn.hashnode.com/res/hashnode/image/upload/v1727925146161/52d4f1a6-86cb-4e52-a18c-2d53d579875e.png align="center")

The fibonacci function is not even close to the top 10 profiling entries. If we open the same profiling report in the flamegrap view you'll see that now it takes less than 10ms to execute.

![Profiling results of improved CPU-intensive endpoint in flamegraph view](https://cdn.hashnode.com/res/hashnode/image/upload/v1727925182974/53e7e0eb-7037-4e05-9c53-89ce2ed76832.png align="center")

Compare this 10ms of execution time to the previous 6.5sec. We can clearly see performance gains.

### Async endpoint

Next, let's explore how to use VS Code's profiler to identify and resolve issues with asynchronous code execution.

Here's an asynchronous function that simulates a time-consuming operation:

```javascript
function generateAsyncOperation() {
 return new Promise(resolve => {
   setTimeout(() => {
     // Simulate a time-consuming asynchronous operation
     for (let i = 0; i < 50000000; i++) { }
       resolve();
   }, 1000);
  });
}
```

For the sake of the example, we're running the `for` loop inside of the `setTimeout` callback just to make things easier to see in the profiler report.

First implementation of the asynchronous endpoint suffer from the waterfall problem where independent asynchronous functions are executed sequentially, causing details as each function waits for the previous one to complete.

```javascript
export async function runAsyncTask(cb) {
 await generateAsyncOperation();
 await generateAsyncOperation();
 await generateAsyncOperation();
 cb();
}
```

Since these asynchronous functions are independent, we don't need to run them sequentially. Instead, we can run them concurrently.

```javascript
export async function runSmartAsyncTask(cb) {
 await Promise.all(new Array(3).fill().map(() => generateAsyncOperation()));
 cb();
}
```

By using `Promise.all` we can run all 3 functions simultaneously.

Now that we've explored the code let's see how profiling can help us identify and address the waterfall problem. You start the debugging session in the same way as we did it for the CPU-intensive endpoint:

1. Open the Debugger tab
    
2. In the "Run and Debug" section, choose the script you want to execute.
    
3. Navigate to the "Call stack" section and click the "Take performance profile" button.
    
4. **Select the "CPU profile" as a profiling option.**
    
5. Choose the "Manual" run option.
    

Let's start by profiling the asynchronous endpoint with the sequential implementation. After sending a request and generating the profiling report, we see the following picture:

![VS Code profiling of sequential asynchronous endpoint with sequential execution in a flamegraph view](https://cdn.hashnode.com/res/hashnode/image/upload/v1727925491436/bba6bcff-489f-4e67-b765-70fe13dca905.png align="center")

Notice those 3 pink entries on the flamegraph. Each one of those entries represents the execution of the `generateAsyncOperation` function.

The time span from the first entry to the last one is almost 2 seconds.

It takes almost 2 seconds from the first entry to the last one. Only after the last entry completes its executed we can get the response from the server.

After identifying the problem we can replace the sequential implementation with the optimized parallel version.

When you finish profiling the new endpoint implementation, the generated profiling report will surprise you.

![VS Code profiling of asynchronous endpoint with parallel execution in a flamegraph view](https://cdn.hashnode.com/res/hashnode/image/upload/v1727925694129/57b89233-f478-40cc-8ef1-bdaea26a675a.png align="center")

Instead of 3 distinct entries there is only 1 representing the concurrent execution of all three asynchronous operations. It takes less than 100ms to complete the requests and return the result.

### Memory leak endpoint

The last type of problems that we'll look into is memory leaks.

Here is how code containing memory leak looks like:

```javascript
const memoryLeak = new Map();

// Function with a memory leak
export function runMemoryLeakTask(cb) {
 for (let i = 0; i < 10000; i++) {
   const person = {
     name: `Person number ${i}`,
	 age: i,
   };
   memoryLeak.set(person, `I am a person number ${i}`);
 }
 cb();
}
```

In this case we assume that data from one request is not required for subsequent requests. Therefore, if any data persists between requests we consider it a memory leak.

Here's a modified version of the code without the memory leak:

```javascript
const smartMemoryLeak = new WeakMap();

// Function without a memory leak
export function runSmartMemoryLeakTask(cb) {
 for (let i = 0; i < 10000; i++) {
   const person = {
     name: `Person number ${i}`,
	 age: i,
   };
   smartMemoryLeak.set(person, `I am a person number ${i}`);
 }
 cb();
}
```

The main difference between these 2 implementations is the type of data used to store the objects. The `Map` in the first example keeps strong references to the object preventing garbage collection even when the object are no longer needed.

In contrast, the `WeakMap` in the second example uses weak references making objects available for garbage collection if the objects themselves are no longer referenced anywhere except the `WeakMap` itself.

We're ready to start profiling. The steps are the same except for the type of profiling we're running. The only difference is that instead of "CPU profile" we use "Heap Profile" option.

![VS Code Heap Profile option from the profiling window](https://cdn.hashnode.com/res/hashnode/image/upload/v1727926304176/ce46beb3-b7ee-45a6-93ab-b6d8589e96ae.png align="center")

To clearly demonstrate the memory leak, let's send 4 requests to both endpoint implementations and compare the results.

Here is the profiling report after sending 4 requests to the endpoint that uses `Map` to store objects.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1727926601885/8b484ec1-ef87-4658-953d-fbcc7d947c5c.png align="center")

After the 4 requests, the program using `Map` occupies around 6mb of memory. While it might not seem like much, let's compare it to the implementation which uses `WeakMap`.

![Result of profiling endpoint without memory leak using VS Code heap profiler](https://cdn.hashnode.com/res/hashnode/image/upload/v1727926625806/5410f935-41e8-4949-84ac-697257a1c4d6.png align="center")

The program that uses `WeakMap` occupies only 350kb of memory after 4 requests. This is less than half a megabyte. The result is that the program occupies 16 times less memory space.

## Conclusion

While VS Code might not have all the advanced features of dedicated profiling tools like DevTools or is not as smart as Clinic.js, it is still a solid option for profiling your Node.js applications.

Especially because you don't need to download any external libraries or connect to external tools and systems. Everything just works within your coding environment, so you stay focused on the goal.

If you want to learn more about other profiling options, I highly recommend reading the previous articles about [how to profile Node.js apps using Chrome DevTools](https://pavel-romanov.com/how-to-profile-nodejs-apps-using-chrome-devtools) and [how smart Clinic.js can help you understand profiling reports better](https://pavel-romanov.com/optimizing-nodejs-identifying-and-fixing-performance-problems-with-clinic).
