The ultimate question you should ask is when to use Node.js and when not to use Node.js. The aim of this article is to provide the answer to that question.
To understand when and when not to use Node, we have to understand the underlying mechanism and the features of the language. Node.js is single-threaded, event-driven, and asynchronous. These words may not mean much to you right now. So let’s look into what each of these features means for how Node.js operates.
Events are at the core of the Node.js architecture. It uses an event loop that listens to events and passes them to event handlers. For example, say you are sending a request from your web browser to a Node.js server. This triggers an event loop in the server that listens to HTTP requests. The event loop then calls the associated callback function to handle the emitted event. At the end of the first event handler’s execution, it again emits another event to pass it on to the next event handler. This process continues until the task is completed. Most modules of Node.js implements an event loop that listens to and responds to events in a loop.
Node.js accepts events in the order they arrive at the event listener. The last event received is added to the end of the event queue and executed after the preceding events are completed.
Node’s event loop uses a single-threaded implementation when handling events. Each event is handled one after another in the order they arrive by a single processor thread. The requests received to a Node web server are served in the order they were received.
To understand the significance of a single-threaded implementation, you have to understand how a multi-threaded implementation works.
A multi-threaded program has a pool of threads that run concurrently to accept requests. The number of threads used by the program is limited by the system’s RAM or other parameters. So, the number of requests it can process at a given time is limited to this number of threads. Whereas the single-threaded Node.js program can process any number of requests at a given time that are in the event queue to be executed.
Node’s single-threaded nature makes programs highly scalable. You can use the same program that is built to handle a limited number of requests to handle a much larger number of requests. Scaling a multi-threaded application calls for more system resources (like RAM) to increase the number of threads in the thread pool.
However, there is a downside to Node.js being single-threaded. The single-threaded implementation makes Node a bad choice for CPU-intensive programs. When a time-consuming task is running in the program it blocks the event loop from moving forward for a longer period. Unlike in a multi-threaded program, where one thread can be doing the CPU-intensive task and others can handle arriving requests, a Node.js program has to wait until the computation completes to handle incoming requests.
Node.js introduced a workaround for this problem in version 10.5.0: worker threads. You can read up more on this topic to understand how to use worker threads to solve this problem for CPU-intensive programs.
You may now have a question? How can a single thread handle more events at a given time than a thread pool if events are handled one after another and not concurrently? This is where Node’s asynchronous model steps in. Even though the Node’s event loop implementation is single-threaded, it achieves a concurrent-like behavior with the use of callbacks.
As I mentioned earlier, Node’s event loop passes an emitted event to a callback function to handle the event. But after passing the task to the callback, the loop doesn’t wait for it to complete running. Instead, it moves forward to the next event. The callback function completes running in the background. https://app.diagrams.net/images/osa_drive-harddisk.png
Assume you want to connect to a separate API from your Node server to retrieve some data. Then you send a request to this particular API from the server, along with this function call, you also pass a callback function with the instruction on what to do when the response from the API is received. The Node program doesn’t wait for the response from the API. Instead, after sending the request, it continues to the next step of the program. When the response from the API arrives the callback function starts running in the background and handles the received data. In a manner, the callback function runs concurrently to the main program thread.
In comparison, in a synchronous multi-threaded program, the thread sending making the API call waits for the response to arrive to continue to the next step. This does not stall the multi-threaded program because, even though this one thread is waiting, other threads in the thread pool are ready to accept receiving requests.
The asynchronous feature is what makes the single-thread of the Node.js program quite efficient. It makes the program fast and scalable without using as many resources as a multi-threaded application.
Naturally, this makes Node a good fit for data-intensive and I/O intensive programs. Data-intensive programs are focus on retrieving and storing data, while I/O-intensive programs focus on interacting with external resources.
Now back to our main question. When should you use Node.js for your projects? Considering the main features of Node.js and its strengths, we can say data-driven, I/O-driven, event-driven, and non-blocking applications benefit the best from a Node.js implementation.
Web servers are a perfect use case that benefits from Node’s inherent features. Node’s event-driven implementation can be used to trigger events in the server every time a request from the client-side is received. The single-threaded and asynchronous features ensure that the server can serve a large number of requests at a given time using callbacks. Sending responses back to the client-side is handled by these callbacks while the event loop keeps accepting and passing the receiving requests to event handlers without much delay.
Especially, web backends that follow microservices architecture, which is now growing in popularity, are well suited for a Node implementation. With the microservices architecture, sending requests from one microservice to another becomes inevitable. With Node’s asynchronous programming, waiting for responses from outside resources can be done without blocking the program thread. Unlike a multi-threaded program, which can run out of threads to serve incoming requests because many threads are waiting for responses from external resources, this I/O-intensive architecture does not affect the performance of the Node application. However, web servers that implement computational-heavy or processes-heavy tasks are not a good fit for Node.js. The programming community is still apprehensive of using Node with a relational database given Node tools for relational databases are still not as developed as compared to other languages.
Real-time applications, like chat applications, video conferencing, and online-gaming, benefit from Node’s inherent set of features. These data-intensive applications require the use of websockets and push technology to establish two-way communication between the server and the client. Node’s ability to handle I/O-intensive tasks and its high scalability are the main reasons why the language is being commonly used for real-time applications. These characteristics make Node apps faster compared to other languages.
Building command-line applications are another common application of Node.js. The simple syntax, fast development, and the number of packages available for this purpose are the reasons why Node has become a popular command-line application language.
Creating APIs with Node.js is gaining increasing popularity. Combine this with the language’s ability to integrate well with NoSQL databases, Node becomes a good fit for creating API fronts for NoSQL databases.
Programs with features that don’t blend well with Node’s characteristics are not a good fit for Node.js development. In general, blocking, processor-intensive, and computational-intensive programs fall under this category.
If your application is likely to run tasks that involve heavy computing and number crunching, like running the Fibonacci algorithm, Node.js is not the best language to turn to. As discussed above, the heavy computation blocks the single-thread running in the application and halts the progress of the event loop until the computation is finished. And it delays serving the requests still in the queue, which may not take as much time.
If your application has a few heavy computational tasks, but in general benefits from Node’s characteristics, the best solution is to implement those heavy computational tasks as background processes in another appropriate language. Using microservices architecture and separating heavy computational tasks from Node implementation is the best solution.
Still, Node’s relational database support tools are not up to the expected level when compared to other languages. This makes Node an undesirable for use cases with relational databases. However, this situation is fast changing. High-quality ORM libraries like Sequealize and TypeORM are already available for Node developers. It may not take long to scratch this point out of the when-not-to-use-node.js list.
Like any programming language out there, Node.js is best suited for some programming tasks and not suitable for some others. Taking advantage of Node’s superiority in different aspects is your job as a programmer. When it comes to Node, its event-driven, single-threaded, and asynchronous features give it superiority in speed and scalability. But they put Node at a disadvantage where computational heavy tasks are considered. Now it’s up to you to take advantage of the language’s better qualities for appropriate situations.