Node.js is a popular runtime environment for building fast, scalable, and efficient web applications. However, even with its inherent strengths, poorly optimized Node.js applications can suffer from performance bottlenecks, resulting in slow load times, unresponsive servers, and increased operating costs. To maximize the potential of your Node.js app and ensure that it can handle high levels of traffic without faltering, it's essential to optimize its performance. In this tutorial, we will explore some effective techniques for optimizing your Node.js app's performance, from analyzing performance metrics to implementing caching strategies and utilizing other best practices. By the end of this tutorial, you will have a comprehensive understanding of how to improve your Node.js app's performance and deliver a superior user experience.
Prerequisites
- Download and install Node.js
- A sample Node.js repo to experiment and test the performance. Fork our sample Node.js application.
Tutorial
Our Node.js application is here - https://github.com/pavanbelagatti/Simple-Node-App
You can see the app.js
in the root of the application's repo,
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3002;
app.get('/', (request, response) => {
response.status(200).json({
message: 'Hello Docker!',
});
});
app.listen(PORT, () => {
console.log(`Server is up on localhost:${PORT}`);
});
You can run the application by using the following command,
node app.js
Now, let's talk about optimizing the performance of this application.
Here are some techniques you can use:
Caching:
Caching can help reduce the amount of time it takes for your application to respond to requests. In Node.js, you can use the node-cache
package to implement caching.
Use asynchronous code:
In Node.js, asynchronous code can help improve the performance of your application.
Use cluster module:
The cluster module can help improve the performance of your Node.js application by allowing it to run on multiple cores.
Let’s add caching, asynchronous code and cluster module techniques to improve the performance of our Node.js application.
Here is the updated app.js
file
const express = require('express');
const cluster = require('cluster');
const numCPUs = require('os').cpus().length;
const NodeCache = require( "node-cache" );
const app = express();
const cache = new NodeCache();
const PORT = process.env.PORT || 3002;
// Define a function to handle the request
const handleRequest = (request, response) => {
// Check if the request is already cached
const key = request.url;
const cachedResponse = cache.get(key);
if (cachedResponse) {
// If the response is cached, return it
console.log(`Serving from cache: ${key}`);
return response.status(200).json(cachedResponse);
}
// If the response is not cached, perform the operation asynchronously
console.log(`Processing request: ${key}`);
setTimeout(() => {
const responseData = {
message: 'Hello Docker!',
};
cache.set(key, responseData);
console.log(`Caching response: ${key}`);
return response.status(200).json(responseData);
}, 1000);
};
// Use clustering to take advantage of multiple CPU cores
if (cluster.isMaster) {
console.log(`Master ${process.pid} is running`);
// Fork workers
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
// Listen for worker exit events
cluster.on('exit', (worker, code, signal) => {
console.log(`Worker ${worker.process.pid} died`);
});
} else {
// Set up the server
app.get('/', handleRequest);
app.listen(PORT, () => {
console.log(`Worker ${process.pid} is up on localhost:${PORT}`);
});
}
The node-cache dependency is missing. Add it using the below command
npm install node-cache
You can run the application with the same command
node app.js
You won’t notice anything as we still have to check how the performance has been improved in the updated server.
The above code includes the following optimizations:
Caching using the node-cache module: We create a new instance of node-cache and use it to store responses for each request URL. When a new request comes in, we first check if the response is already cached. If it is, we return the cached response instead of generating a new response.
Asynchronous code: We use the setTimeout function to simulate an asynchronous operation that takes 1 second to complete. This allows the server to continue processing requests while the asynchronous operation is running.
Cluster module: We use the cluster module to create a worker process for each CPU core. This allows the server to handle multiple requests simultaneously and take advantage of all available CPU cores.
Let’s test the performance of the old vs. optimized server.
To test the performance of the servers under load, you can use a load testing tool such as Apache JMeter or Siege. These tools allow you to simulate a large number of concurrent users making requests to your server and measure the response time, throughput, and other performance metrics.
Here's an example of how you can use Siege to test the performance of your servers:
Install Siege: You can install Siege using the following command on Unix-based systems:
sudo apt-get install siege
And for MAC, you can use the below command to install Siege.
brew install siege
Once Siege is installed, you can run the following command to simulate 100 concurrent users making 1000 requests to your server:
siege -c 100 -r 1000 http://localhost:3002/
Before this, make sure your server is up using the following command,
node app.js
Then, run the Curl command to measure the response time
time curl http://localhost:3002/
The output can be broken down into the following components:
- 0.00s user: This is the amount of time that the CPU spent executing user code (i.e., the time spent executing your JavaScript code).
- 0.00s system: This is the amount of time that the CPU spent executing system code (i.e., the time spent executing code in the operating system or other system-level code).
- 39% cpu: This is the percentage of CPU usage during the execution of the code.
- 0.018 total: This is the total time it took to execute the code (i.e., user time + system time).
Now, let’s compare the performance with the old server.
Follow the same method, just make sure that your old server (app.js) is running on your local 3001 port for this experiment.
Run the application
node app.js
Add the load with Siege
siege -c 100 -r 1000 http://localhost:3001/
Then, run the Curl command to measure the response time
time curl http://localhost:3001/
- CPU spent executing code: 0.00s
- Time spent executing system code: 0.01s
- % of CPU usage for executing the code: 51%
- Time taken to execute the code: 0.020
It is evident and more obvious which server is more optimized.
It is important to note that optimizing your Node.js app's performance is an ongoing process that requires continuous monitoring and tweaking. By implementing these techniques, you can significantly improve the performance of your app and ensure that it meets the needs of your users. With proper optimization, your Node.js app can handle high traffic volumes and deliver a seamless user experience, which is crucial for the success of any online business or application.
If you enjoyed this article, be sure to check out my other articles on Node.js.
Top comments (3)
Thanks for this list. I think that while caching can always be added later as an optimization (really no need to do it advance), the async part should be part of the design, changing sync to async as an optimization in non-trivial code can be very costly.
I think one of the biggest things people will need to learn is how to bust their cache. After an update on those endpoints they need to map which keys to bust
Redis is a must-have imho