Node.js Mastery: Advanced Development & Performance Tips

4EYc...59yB
12 Jan 2024
25

Are you a front-end enthusiast diving into the realm of back-end development? 🤔 Brace yourself for a thrilling journey through the intricate landscapes of Node.js! 🎢

In this adventure, we’ll explore a myriad of backend wonders, from the swift creation of your own backend using Node.js frameworks to tackling performance analysis, testing, and diving deep into memory management. 💻🔍

Join us as we unravel the mysteries of C++ plug-ins, conquer sub-processes, master the art of multi-threading, harness the power of Cluster modules, and seamlessly manage process daemons. 🚀💡

It’s time to elevate your Node.js skills and conquer the backend universe! 🌌 Are you ready for the challenge? Let’s dive in! 🚀👩‍💻👨‍💻

Building Your First HTTP Server with Express
Alright, buckle up for an exciting journey into the world of building basic services! 🎢💻 To kick things off, let’s roll up our sleeves and implement a straightforward HTTP server. And for the sake of simplicity, we’re going to embrace the awesomeness of Express! 🚀🚂

const fs = require('fs')
const express = require('express')
const app = express()

app.get('/', (req, res) => {
res.end('hello world')
})

app.get('/index', (req, res) => {
const file = fs.readFileSync(__dirname + '/index.html', 'utf-8')
/* return buffer */
res.end(file)
/* return stream */
// fs.createReadStream(__dirname + '/index.html').pipe(res)
})

app.listen(3000)
Typically, our backend services collaboratively return a series of interface information. However, for certain subsequent tests, we’ve opted to return a file. This choice allows us to gauge our service performance and identify potential bottlenecks more intuitively, especially with larger datasets.

đź“„ Additionally, in the comment stream, you can observe that we also use a form to return information. When returning a file, the initial synchronous reading process tends to be more time-consuming. With larger files, the entire content is stored in memory before being returned at once. This performance issue becomes more noticeable, particularly in terms of memory usage.

Performance Testing and Stress Testing
To evaluate our system’s robustness under high concurrency, we’ll utilize testing tools specifically designed for stress testing. I recommend considering two reliable tools:

1. ab (Apache Benchmark)
2. webbench
3. autocannon

For our purposes, let’s focus on ab for the next steps. Apache Benchmark, or ab, is a tool from the Apache Software Foundation. If you’re on a macOS, you’re in luck as this tool is already included. For other systems, you can find installation tutorials online.

⚠️ Keep in mind that the ab tool that comes with macOS has concurrency restrictions.

Now, let’s dive into a simple command and break down the key parameters:

ab -n <total_requests> -c <concurrent_requests> <url>
- -n: Total number of requests to perform during the test.
- -c: Number of multiple requests to perform at a time.

Example:

ab -n 1000 -c 10 http://your-api-endpoint
This command initiates a stress test with 1000 total requests, simulating 10 concurrent requests at a time.

Node.js Performance Analysis Tool
profile
Node.js comes equipped with a powerful profiling tool. To utilize it, simply add — prof when starting your Node.js application:

node - prof index.js
Upon starting the server, a file named isolate-0x104a0a000–25750-v8.log will be immediately generated in the directory. Initially, you can disregard the file. Now, let’s conduct a 15-second stress test:

ab -c 50 -t 15 http://127.0.0.1:3000/index
After completing the stress test, our log file will have changed. However, the data inside is extensive and requires parsing.

To make the data more accessible, we can use the following command that comes with Node.js:

node - prof-process isolate-0x104a0a000–25750-v8.log > profile.txt
This command converts the generated log file into a more readable txt format, storing it in the current directory. Although this text format is more intuitive, it might still be a bit inconvenient. The content includes the number of calls, occupied time, various call stack information for JavaScript, C++, garbage collection, and more. You can explore the details manually.

If the text format doesn’t suit your needs, there’s another method for more convenient analysis! 📊

Chrome DevTools
Given that Node.js is powered by the Chrome V8 engine, we can leverage Chrome DevTools to debug Node.js effectively. To initiate debugging and pause the program simultaneously, we’ll use the new parameter ` — inspect-brk`:

node - inspect-brk index.js
Upon running this command, you’ll see a message indicating that the debugger is listening on a WebSocket, such as `ws://127.0.0.1:9229/e9f0d9b5-cdfd-45f1–9d0e-d77dfbf6e765`.

For debugging, open your Chrome browser and navigate to `chrome://inspect` in the address bar.

🔍 Here, you’ll find a list of available targets for inspection. Locate your Node.js application, click “inspect,” and voila! You are now set to debug your Node.js application using the powerful Chrome DevTools.

For more guidance, you can refer to the official Node.js documentation: [Node.js Inspector](https://nodejs.org/en/docs/inspector).

Code performance optimization
Upon analyzing the performance bottlenecks, it’s evident that the readFileSync operation, responsible for reading the code, consumes the most time. Further examination of the original code reveals that every access to the /index path triggers a re-read of the file. This becomes a clear optimization opportunity.

Let’s make a slight modification to enhance performance:

const fs = require('fs')
const express = require('express')
const app = express()
app.get('/', (req, res) => {
res.end('hello world')
})
/* When extracted to the outside, each program will only read it once to improve performance */
const file = fs.readFileSync(__dirname + '/index.html', 'utf-8')
app.get('/index', (req, res) => {
/* return buffer */
res.end(file)
/* return stream */
// fs.createReadStream(__dirname + '/index.html').pipe(res)
})
app.listen(3000)
After our transformative analysis and stress testing, the results speak for themselves — optimizing file reading by employing asynchronous operations can double your QPS (Queries Per Second). This straightforward code modification demonstrates the substantial impact of efficient coding practices.

🛠️ Another optimization point lies in understanding the underlying operations. For instance, removing the specified format in readFileSync and allowing it to default to Buffer significantly improved QPS during stress testing. This highlights the advantage of leveraging buffer operations for enhanced efficiency and performance.

While there are numerous optimization possibilities, tackling major bottlenecks, like file reading, provides substantial gains. It’s essential to grasp the principles and techniques for optimization. The key takeaway is to consider moving time-consuming operations or calculations before the service starts to enhance overall performance.

Performance Optimization Guidelines
1. Reduce Unnecessary Calculations:
— Node.js calculations, such as file encoding/decoding, can consume a significant portion of CPU resources. Minimize these operations whenever possible to enhance performance.

2. Space for Time Strategy:
— Optimize by caching results, especially for frequent operations like file reads or calculations. Reusing cached data reduces redundant computations and speeds up subsequent requests.

3. Advance Calculations During Startup:
— Consider moving time-consuming calculations from runtime to the startup phase of your service. By performing calculations in advance during initialization, you can achieve substantial performance improvements.

Memory management
Garbage Collection in JavaScript 🗑️
JavaScript’s memory management is handled by the language itself, employing the Garbage Collection (GC) mechanism. This mechanism comprises two segments: the new generation and the old generation.

1. New Generation:
— Newly created variables initially enter the new generation. When the new generation’s memory approaches full capacity, a garbage collection is triggered to clear redundant variables, making space for new ones. This process ensures efficient memory utilization.

2. Old Generation:
— Variables that persist through multiple garbage collections are considered long-lived and are moved to the old generation. The old generation has a larger capacity but undergoes slower garbage collection.

đź’ˇ Key Points:
- New Generation: Smaller capacity, faster garbage collection.
- Old Generation: Larger capacity, slower garbage collection.

Therefore, monitoring and addressing memory-related issues are essential for maintaining optimal performance. 🚀🔍

Controlling Memory Usage in Node.js 🧠
Node.js employs a specific memory allocation strategy for its Buffer based on file sizes. The approach differs for files smaller than 8KB and those larger than 8KB.

1. Files Smaller than 8KB:
— Node.js avoids frequent creation of small files by allocating an 8KB space initially.
— Upon obtaining the space, if the required buffer is less than 8KB, a portion is extracted from the allocated 8KB in sequential order.
— If encountering a buffer smaller than 8KB, additional space is allocated as needed.
— Released space from destroyed variables becomes available for later use.

2. Files Larger than 8KB:
— For larger files, Node.js dynamically allocates memory as needed.

🚀 Optimization Strategy:
— Adopt a strategy similar to a memory pool if you encounter memory constraints in your coding.
— Leverage efficient memory management practices to optimize your code.

Understanding and utilizing Node.js Buffer’s memory allocation mechanism provides insights into effective memory usage, helping optimize performance.

Conclusion 🌟
Embarking on the journey of Node.js mastery has unveiled a treasure trove of advanced development and performance optimization tips.

From building your first HTTP server with Express to delving into memory management and performance testing, each step unlocks new dimensions of backend expertise. Armed with insights into Chrome DevTools, code optimization, and memory control strategies, you’re now equipped to conquer the complexities of Node.js development.

The backend universe awaits your exploration — are you ready to soar to new heights? Happy coding! 🚀👩‍💻👨‍💻

Write & Read to Earn with BULB

Learn More

Enjoy this blog? Subscribe to Piebae

0 Comments

B
No comments yet.
Most relevant comments are displayed, so some may have been filtered out.