Node.js: boost perf with worker threads

Node.js: boost perf with worker threads

Node.js is a powerful and efficient tool.

However, because it is single threaded we might have issues when working with CPU-intensive operations. For example, when you try to upload large JSON or media files. This operation will most likely block the main thread and every other operation will have to wait until it’s completed.

First off, what is a worker thread in Node.js?

A worker thread is a sequence of instructions within a program that can be executed independently of other code. Worker threads are a part of a process and share the resources allocated to that process, which is why they’re more efficient than child processes.

Let’s take a look at a quick example:

Initialize a node project with yarn init -y, install express yarn add express and create a source file index.js and then add the following code to it.

const express = require("express");
const app = express();

app.get("/", (req, res) => {
  res.send("Hello World");
});

app.get("/heavy", async (req, res) => {
  let total;
  for (let index = 0; index < 100000000000; index++) {
    total += index;
  }
  res.send(total);
});

app.listen(8080);

In the code above, we have two endpoints, the one that returns Hello World and the one that runs a loop and returns the count of the total iterations. I intentionally added a very large number as the length of the loop to simulate a heavy operation.

If we run this code as it’s and try to call the /heavy endpoint the whole app will stop working as the operation will block the thread. Even the endpoint that returns simple Hello World won’t work until the loop is complete.

We need to fix that.

We need to allow other users to use our app regardless of other users’ operations. One user should not stop our entire app from functioning.

There are several ways to fix it by using Worker Threads, Clusters, and Child Processes. I choose to use worker threads because it seems to be more efficient.

Since Worker Threads are not an external dependency we’ll simply require it and use it like so;

const express = require("express");
const app = express();
const { Worker } = require("worker_threads");

const worker = new Worker("./worker");

app.get("/", (req, res) => {
  res.send("Hello");
});

app.get("/heavy", async (req, res) => {
  worker.on("message", (data) => {
    res.send(data);
  });
});

app.listen(8080);

In the code above, we required worker_threads, created an instance of it, now we need to create that worker thread in a different file.

//worker.js

const { parentPort } = require("worker_threads");

let total;
for (let index = 0; index < 100000000000; index++) {
  total = index;
}

parentPort.postMessage(total);

Now, we’ve off-loaded the entire heavy-duty to the worker, and then we are using the postMessage event channel to send a notification to the worker thread when the heavy operation is completed while the worker awaits the message event to process the returned data and send it to the client as shown here;

app.get("/heavy", async (req, res) => {
  worker.on("message", (data) => {
    res.send(data);
  });
});

With this now, if this endpoint is processing a CPU-heavy task it won’t block the main thread and your app will become fast and able to withstand pressure to a large extent.

Further reading:

I’ll encourage you to take a look at Child Processes, Clusters, and deep dive on worker threads from the docs.

Happy Hacking!

Buy Me A Coffee

Published by Eze Sunday Eze

Hi, welcome to my blog. I am Software Engineer and Technical Writer. And in this blog, I focus on sharing my views on the tech and tools I use. If you love my content and wish to stay in the loop, then by all means share this page and bookmark this website.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.