Profiling Node.js Applications: Step-by-Step Guide

NonCoderSuccess
3 min readNov 10, 2024

--

Introduction to Profiling Profiling a Node.js app means analyzing CPU, memory, and runtime metrics to identify performance issues like high CPU usage, memory leaks, or slow functions. By profiling, we can detect bottlenecks and improve app speed, responsiveness, and scalability.

Tools for Profiling

While many third-party tools are available, Node.js also has a built-in profiler. The profiler uses the V8 engine, which samples the call stack periodically, recording data points like function calls and optimization events (e.g., JIT compiles).

Example App: User Authentication with Express

Let’s explore profiling with an Express app that has two routes:

  1. /newUser – Adds a new user with a hashed password.
  2. /auth – Authenticates a user’s login request.

New User Handler

app.get('/newUser', (req, res) => {
let username = req.query.username || '';
const password = req.query.password || '';
username = username.replace(/[!@#$%^&*]/g, '');

if (!username || !password || users[username]) return res.sendStatus(400);
const salt = crypto.randomBytes(128).toString('base64');
const hash = crypto.pbkdf2Sync(password, salt, 10000, 512, 'sha512');
users[username] = { salt, hash };
res.sendStatus(200);
});

Authentication Handler

app.get('/auth', (req, res) => {
let username = req.query.username || '';
const password = req.query.password || '';
username = username.replace(/[!@#$%^&*]/g, '');
if (!username || !password || !users[username]) return res.sendStatus(400);
const { salt, hash } = users[username];
const encryptHash = crypto.pbkdf2Sync(password, salt, 10000, 512, 'sha512');
if (crypto.timingSafeEqual(hash, encryptHash)) res.sendStatus(200);
else res.sendStatus(401);
});

Note: This example is for educational purposes. In production, use established libraries for user authentication.

Running the Profiler

Assume the app is deployed, and users report latency issues. To profile it:

  1. Run the app with the profiler:
NODE_ENV=production node --prof app.js

2. Simulate load with ApacheBench (ab):

curl -X GET "http://localhost:8080/newUser?username=matt&password=password" 
ab -k -c 20 -n 250 "http://localhost:8080/auth?username=matt&password=password"

Output Example:

Concurrency Level:      20
Time taken for tests: 46.932 seconds
Requests per second: 5.33 [#/sec]
Time per request: 3754.556 [ms] (mean)

The app handles only ~5 requests per second, with an average request time of 4 seconds, indicating a bottleneck.

Analyzing the Profiler Output

After running with --prof, a log file (e.g., isolate-0x...-v8.log) is generated. To analyze it:

  1. Process the file:
node --prof-process isolate-0x...-v8.log > processed.txt

2. Open processed.txt in a text editor. The summary shows time spent by each part of the code:

[Summary]:   
ticks total nonlib name
79 0.2% 0.2% JavaScript
36703 97.2% 99.2% C++
7 0.0% 0.0% GC

Most time (97%) is spent in C++, meaning the core work is outside JavaScript.

3. In the [C++] section, specific functions that consume the most CPU time are shown:

[C++]:   
ticks total nonlib name
19557 51.8% 52.9% node::crypto::PBKDF2
4510 11.9% 12.2% _sha1_block_data_order

Observation: The PBKDF2 function, which hashes passwords, takes over half of the CPU time.

4. To further understand, check the [Bottom up (heavy) profile] section, which shows the functions calling PBKDF2:

ticks  parent  name 
19557 51.8% node::crypto::PBKDF2
19557 100.0% LazyCompile: *pbkdf2 crypto.js:557

Optimizing the Code

The profiling reveals that synchronous password hashing (PBKDF2) blocks the event loop. Switching to asynchronous hashing will allow other requests to proceed while a hash is computed.

Updated Asynchronous Handler

app.get('/auth', (req, res) => {
let username = req.query.username || '';
const password = req.query.password || '';
username = username.replace(/[!@#$%^&*]/g, '');
if (!username || !password || !users[username]) return res.sendStatus(400);
crypto.pbkdf2(password, users[username].salt, 10000, 512, 'sha512', (err, hash) => {
if (users[username].hash.toString() === hash.toString()) res.sendStatus(200);
else res.sendStatus(401);
});
});

Re-Testing Performance

Run the app again with ab. The results should now show a significant performance improvement.

Updated Output Example:

Concurrency Level:      20
Time taken for tests: 12.846 seconds
Requests per second: 19.46 [#/sec]

With asynchronous hashing, the app handles about 20 requests per second — a 4x increase — and reduces average request time to ~1 second.

Conclusion

By analyzing the profiler output, we identified a CPU-heavy function and optimized it with asynchronous code, improving app responsiveness. The Node.js profiler can be a powerful tool for understanding and optimizing your application’s performance.

Next Read :

--

--

NonCoderSuccess
NonCoderSuccess

Written by NonCoderSuccess

Welcome to NonCoderSuccess, Making tech easy for everyone. I share simple tutorials and insights to help succeed. Follow for tech tips and guides!

Responses (1)