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!