Multithreading in Node: Boosting Node.js Performance with Worker Threads

Animesh MondalAnimesh Mondal
4 min read

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 in index.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 -

  1. We imported Worker class from worker_thread , Worker is an independent JavaScript execution thread.

  2. new Worker('./worker.js')` This represents the path to the Worker’s main script.

  3. workerData option allows us to pass any value to the worker we created.

  4. worker.once('message') The 'message' event is emitted once for any incoming message from the worker thread.

  5. worker.on('error') The 'error' event is emitted if the worker thread throws an uncaught error.

  6. 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 -

  1. parentPort is the communication channel that allows the child thread to communicate back with the main thread (Parent thread).

  2. parentPort.postMessage(data) is used to send back the data to the main thread.

  3. 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

1
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.