Multithreading in Node: Boosting Node.js Performance with Worker Threads
Node.js runs the JavaScript code using only a single thread, which means only one statement can be executed at a time. This limitation can be problematic when dealing with CPU-intensive tasks (encryption, decryption, compression, decompression, etc.), that will block the main thread
and ruin user experience (how will explain in a moment). To address this issue, Node.js introduced worker threads
, which allow us to execute JavaScript code in parallel across multiple threads without blocking the main thread.
Let's take an example and see how things will work without multithreading.
First, we need to initialize the npm project with
npm init -y
Install express dependency with
npm i express
Create an
index.js
file with the following code
const express = require("express");
const app = express();
const PORT = 8000;
app.get("/", (req, res) => {
//CPU intensive task starts
function fib(n) {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
}
let ans;
for (let i = 0; i <= 10; i++) {
ans = fib(40);
}
//CPU intensive task ends
res.json({ fibonacci: ans });
});
app.get("/hello", (req, res) => {
res.send("hello world");
});
app.listen(PORT, () => {
console.log(`app listening on : http://localhost:${PORT}`);
});
Now start the express application, then hit the endpoint http://localhost:8000
in the browser. The code inside index.js is calculating the factorial again and again, so we can expect it to give a response in around 10 or 15 seconds (you can check it through postman).
While waiting, open another tab and enter the endpoint http://localhost:8000/hello
.
This route sends a simple hello world
and it should open immediately, but it won’t be displayed before the factorial calculation is done. The reason is simple, the main thread is blocked by the factorial calculation so it would not be able to serve other requests.
Now let's see how worker threads module can solve this issue...
- Require the worker thread module in
index.js
file
//index.js
const { Worker } = require("worker_threads");
- Change the
"/"
route code inindex.js
with the following
//index.js
app.get("/", (req, res) => {
const worker = new Worker("./worker.js", { workerData: { num: 40 } });
worker.once("message", (e) => {
console.log(e);
res.json(e);
});
worker.on("error", (err) => {
console.log(err);
});
worker.on("exit", (exitCode) => {
console.log(`It exited with code ${exitCode}`);
});
});
Let's understand what is going on in index.js
file -
We imported
Worker
class fromworker_thread
,Worker
is an independent JavaScript execution thread.new Worker('./worker.js')
` This represents the path to the Worker’s main script.workerData
option allows us to pass any value to the worker we created.worker.once('message')
The'message'
event is emitted once for any incoming message from the worker thread.worker.on('error')
The'error'
event is emitted if the worker thread throws an uncaught error.worker.on('exit')
The'exit'
event is emitted once the worker has stopped its execution.
Your final index.js
file should look like this
const express = require("express");
const { Worker } = require("worker_threads");
const app = express();
const PORT = 8000;
app.get("/", (req, res) => {
const worker = new Worker("./worker.js", { workerData: { num: 40 } });
worker.once("message", (e) => {
res.json(e);
});
worker.on("error", (err) => {
console.log(err);
});
worker.on("exit", (exitCode) => {
console.log(`It exited with code ${exitCode}`);
});
});
app.get("/hello", (req, res) => {
res.send("hello world");
});
app.listen(PORT, () => {
console.log(`app listening on : http://localhost:${PORT}`);
});
- Now create another file
worker.js
and add the following code
//worker.js
const { parentPort, workerData } = require("worker_threads");
parentPort.postMessage(calcFib(workerData.num));
function calcFib(num) {
//CPU intensive task starts
function fib(n) {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
}
let ans;
for (let i = 0; i <= 10; i++) {
ans = fib(num);
}
//CPU intensive task ends
return `The fibonacci number of ${workerData.num} is ${ans}`;
}
We moved all CPU-intensive tasks in the worker.js
file, some points to be noted here -
parentPort
is the communication channel that allows the child thread to communicate back with the main thread (Parent thread).parentPort.postMessage(data)
is used to send back the data to the main thread.We are getting to value passed to the worker thread using
workerData
.
Now start the express application, then hit the endpoint http://localhost:8000
The code inside worker.js
is nothing but calculating fibonacci again and again, so we can expect to get the response in around 10 or 15 seconds. While waiting, open another tab and enter the endpoint http://localhost:8000/hello
. In this case, we will get the response within no time (you can check using postman).
This is because the CPU-intensive task is being processed by a separate thread so the main thread is not blocked at this time.
It allows the application to process any new incoming requests, even while it is handling existing requests.
References
Subscribe to my newsletter
Read articles from Animesh Mondal directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Animesh Mondal
Animesh Mondal
MERN Developer and WEB 3.0 Enthusiast.