Table of contents
1.
Introduction
2.
What are Web Workers in JS?
3.
Why Use JavaScript Web Workers?
4.
How do We Exchange Data With a Worker?
4.1.
Sending Messages to a Worker
4.2.
Receiving Messages in a Worker:
4.3.
Receiving Messages in the Main Thread
5.
Setting Up Web Workers & Multi-threading to JavaScript
5.1.
Creating a Worker Script
5.2.
Creating a Worker Instance
5.3.
Sending Data to the Worker
5.4.
Receiving Data from the Worker
5.5.
Terminating a Worker
5.6.
Handling Errors
6.
Asynchronous Programming
6.1.
Callbacks
6.2.
Promises
6.3.
Async/Await
7.
Frequently Asked Questions
7.1.
Can web workers access the DOM?
7.2.
What types of tasks are suitable for web workers?
7.3.
How do you terminate a web worker from the main thread?
8.
Conclusion 
Last Updated: Jul 8, 2024
Easy

Multithreading in JavaScript

Author Gaurav Gandhi
0 upvote
Career growth poll
Do you think IIT Guwahati certified course can help you in your career?

Introduction

JavaScript is a popular programming language used for creating interactive web pages. It allows developers to add dynamic elements to their websites, which makes them more engaging & responsive. However, JavaScript is single-threaded, which means it can only execute one task at a time. This can be a problem when dealing with CPU-intensive tasks, as they can block the main thread & make the website unresponsive. This is where multithreading comes in. Multithreading allows JavaScript to execute multiple tasks simultaneously, improving performance & responsiveness. 

Multithreading in JavaScript

In this article, we will learn about the concept of multithreading in JavaScript, and what web workers are, why they are used, how to exchange data with them, & how to set up multithreading in your JavaScript code.

What are Web Workers in JS?

Web workers are a feature in JavaScript that allows you to run scripts in the background, separate from the main thread of execution. They enable you to perform tasks that are computationally expensive or time-consuming without blocking the user interface or making the web page unresponsive.

When you create a web worker, it runs in a separate thread, independent of the main JavaScript thread. This means that the worker can perform its tasks without interfering with the responsiveness of the web page. The main thread & the worker thread communicate with each other through a messaging system, allowing them to exchange data & coordinate their activities.

Simple example of how to create a web worker in JavaScript:

// Main script

const worker = new Worker('worker.js');
worker.onmessage = function(event) {
  console.log('Received message from worker:', event.data);
};
worker.postMessage('Hello from the main script!');


// Worker script (worker.js)

self.onmessage = function(event) {
  console.log('Received message from main script:', event.data);
  self.postMessage('Hello from the worker!');
};


In this example, the main script creates a new web worker by instantiating the Worker object & specifying the URL of the worker script file (worker.js). The onmessage event handler is used to listen for messages sent from the worker, & the postMessage method is used to send messages to the worker.

The worker script, defined in worker.js, also has an onmessage event handler to receive messages from the main script. It can perform its tasks & send messages back to the main script using postMessage.

Note: Web workers are useful for offloading heavy computations, processing large amounts of data, or performing background tasks without affecting the responsiveness of the main thread.

Why Use JavaScript Web Workers?

JavaScript web workers offer several benefits that make them valuable for certain scenarios. Here are some key reasons to use web workers:

  1. Improved Performance: Web workers allow you to offload intensive or time-consuming tasks to a separate thread, preventing them from blocking the main thread. This can significantly improve the performance of your web application, especially when dealing with complex computations or large datasets. By running these tasks in the background, the main thread remains responsive, ensuring a smooth user experience.
     
  2. Responsiveness: When executing long-running tasks or heavy computations directly in the main thread, the web page may become unresponsive or frozen until the task is completed. This can lead to a poor user experience, as the user may perceive the application as slow or unresponsive. Web workers mitigate this issue by running such tasks in a separate thread, allowing the main thread to continue responding to user interactions & rendering the user interface.
     
  3. Parallel Processing: Web workers enable parallel processing in JavaScript. By distributing tasks across multiple workers, you can leverage the power of multi-core processors & perform computations simultaneously. This can significantly speed up the execution of certain algorithms or processes that can be parallelized, such as image processing, data analysis, or complex mathematical calculations.
     
  4. Asynchronous Operations: Web workers are well-suited for handling asynchronous operations, such as making API requests, reading or writing files, or performing database operations. By moving these operations to a worker thread, you can prevent them from blocking the main thread & ensure that the user interface remains responsive while waiting for the operations to complete.
     
  5. Modularity & Separation of Concerns: Web workers allow you to separate the logic of your application into different modules or components. You can encapsulate specific functionality or tasks within a worker, making your codebase more modular & easier to maintain. This separation of concerns helps improve code organization & reusability.
     
  6. Enhanced User Experience: By leveraging web workers, you can provide a smoother & more responsive user experience. Users can continue interacting with the web page while background tasks are being executed, without experiencing any freezing or lag. This can lead to increased user satisfaction & engagement with your application.

How do We Exchange Data With a Worker?

To exchange data between the main thread & a web worker, you can use the postMessage() method to send messages & the onmessage event handler to receive messages. Let's explore this in more detail.

Sending Messages to a Worker

To send a message from the main thread to a worker, you can use the postMessage() method on the worker object. The message can be a primitive value (such as a string or number) or an object. 

For example:

// Main script

const worker = new Worker('worker.js');
worker.postMessage('Hello from the main script!');
worker.postMessage({ data: [1, 2, 3, 4, 5] });


In this example, the main script sends two messages to the worker: a string message & an object containing an array of numbers.

Receiving Messages in a Worker:

To receive messages in a worker, you need to define an onmessage event handler. This handler will be invoked whenever a message is sent from the main thread to the worker. The message data can be accessed through the data property of the event object. 

For example:

// Worker script (worker.js)

self.onmessage = function(event) {
  console.log('Received message from main script:', event.data);
  // Process the received data
  const result = event.data.map(item => item * 2);
  self.postMessage(result);
};


In this example, the worker script receives the message sent from the main thread. It logs the received data & then processes it by multiplying each item in the array by 2. Finally, it sends the result back to the main thread using postMessage().

Receiving Messages in the Main Thread

To receive messages sent from a worker in the main thread, you need to define an onmessage event handler on the worker object. This handler will be invoked whenever the worker sends a message using postMessage(). 

For example:

// Main script

const worker = new Worker('worker.js');
worker.onmessage = function(event) {
  console.log('Received message from worker:', event.data);
  // Process the received data
  const result = event.data.reduce((sum, item) => sum + item, 0);
  console.log('Sum of the processed data:', result);
};


In this example, the main script receives the message sent from the worker. It logs the received data & then processes it by calculating the sum of the items in the array using the reduce() method. Finally, it logs the sum of the processed data.

It's important to note that the data exchanged between the main thread & the worker is copied, not shared. This means that any changes made to the data in one thread will not affect the data in the other thread. If you need to share complex data structures or objects between the main thread & the worker, you can use the structured clone algorithm to create a deep copy of the data.

Setting Up Web Workers & Multi-threading to JavaScript

Now we will learn about setting up web workers & implementing multi-threading in JavaScript. Let’s see how to do this:

Creating a Worker Script

Start by creating a separate JavaScript file that will serve as the worker script. This script will contain the code that runs in the worker thread. 

For example, let's create a file named worker.js:

// worker.js

self.onmessage = function(event) {
  const data = event.data;
  // Perform some computation or task with the received data
  const result = data.map(item => item * 2);
  self.postMessage(result);
};


In this example, the worker script receives data from the main thread, performs a computation (multiplying each item by 2), & sends the result back to the main thread.

Creating a Worker Instance

In your main JavaScript file, create a new instance of the Worker object, specifying the URL of the worker script file:

// main.js

const worker = new Worker('worker.js');


This code creates a new worker instance & associates it with the worker.js script.

Sending Data to the Worker

To send data from the main thread to the worker, use the postMessage() method on the worker instance:

// main.js

const data = [1, 2, 3, 4, 5];
worker.postMessage(data);


In this example, we send an array of numbers to the worker using postMessage().

Receiving Data from the Worker

To receive data sent from the worker, define an onmessage event handler on the worker instance:

// main.js

worker.onmessage = function(event) {
  const result = event.data;
  console.log('Received result from worker:', result);
};


The onmessage event handler is invoked whenever the worker sends data back using postMessage(). The received data can be accessed through the data property of the event object.

Terminating a Worker

If you no longer need a worker, you can terminate it using the terminate() method:

// main.js

worker.terminate();


Terminating a worker will stop its execution & free up any resources it was using.

Handling Errors

To handle errors that may occur within a worker, you can define an onerror event handler on the worker instance:

// main.js

worker.onerror = function(event) {
  console.error('Error in worker:', event.message);
};


The onerror event handler is invoked if an error occurs within the worker script. You can log the error message or take appropriate action based on the error.

Asynchronous Programming

Asynchronous programming allows you to initiate long-running operations without waiting for them to complete, enabling your code to continue executing other tasks in the meantime.

JavaScript provides several mechanisms for asynchronous programming, such as callbacks, promises, & async/await. Let's explore how web workers can be used with these techniques:

Callbacks

Callbacks are a traditional approach to handling asynchronous operations in JavaScript. When using web workers with callbacks, you can pass a callback function to the worker & invoke it when the worker completes its task. 

For example:

// main.js

const worker = new Worker('worker.js');
worker.onmessage = function(event) {
  const result = event.data;
  console.log('Received result from worker:', result);
};
worker.postMessage({ data: [1, 2, 3, 4, 5], callback: 'processData' });


// worker.js

self.onmessage = function(event) {
  const { data, callback } = event.data;
  // Perform some computation or task with the received data
  const result = self[callback](data);
  self.postMessage(result);
};

function processData(data) {
  return data.map(item => item * 2);
}


In this example, the main script sends data along with a callback function name to the worker. The worker receives the data, invokes the specified callback function (processData), & sends the result back to the main thread.

Promises

Promises provide a more structured & readable way to handle asynchronous operations. You can use promises to wrap the communication between the main thread & the worker. 

For  example:

// main.js

function runWorker(data) {
  return new Promise((resolve, reject) => {
    const worker = new Worker('worker.js');
    worker.onmessage = function(event) {
      resolve(event.data);
    };
    worker.onerror = function(error) {
      reject(error);
    };
    worker.postMessage(data);
  });
}

runWorker([1, 2, 3, 4, 5])
  .then(result => {
    console.log('Received result from worker:', result);
  })
  .catch(error => {
    console.error('Error in worker:', error);
  });

// worker.js
self.onmessage = function(event) {
  const data = event.data;
  // Perform some computation or task with the received data
  const result = data.map(item => item * 2);
  self.postMessage(result);
};


In this example, the runWorker function returns a promise that resolves with the result received from the worker. The main script sends data to the worker & waits for the promise to resolve. The worker script processes the data & sends the result back, which is then handled by the promise's then block.

Async/Await

Async/await is a modern syntax built on top of promises that makes asynchronous code look & behave more like synchronous code. You can use async/await with web workers to simplify the communication & error handling. Here's an example:

// main.js

async function runWorker(data) {
  const worker = new Worker('worker.js');
  return new Promise((resolve, reject) => {
    worker.onmessage = function(event) {
      resolve(event.data);
    };
    worker.onerror = function(error) {
      reject(error);
    };
    worker.postMessage(data);
  });
}
(async function() {
  try {
    const result = await runWorker([1, 2, 3, 4, 5]);
    console.log('Received result from worker:', result);
  } catch (error) {
    console.error('Error in worker:', error);
  }
})();


// worker.js

self.onmessage = function(event) {
  const data = event.data;
  // Perform some computation or task with the received data
  const result = data.map(item => item * 2);
  self.postMessage(result);
};


In this example, the runWorker function is marked as async & returns a promise. The main script uses the await keyword to wait for the promise to resolve & retrieves the result from the worker. Error handling is done using a try/catch block.

By combining web workers with asynchronous programming techniques, you can achieve efficient & non-blocking execution of tasks, improving the overall performance & responsiveness of your JavaScript applications.

Note: Remember to choose the appropriate asynchronous programming approach based on your specific requirements & the compatibility of the target environments where your code will run.

Frequently Asked Questions

Can web workers access the DOM?

No, web workers operate in a separate context & cannot directly interact with the DOM. This isolation helps maintain thread safety & enhances performance by avoiding direct manipulation of the web page elements.

What types of tasks are suitable for web workers?

Tasks that are computationally intensive and do not require DOM interaction, such as data fetching, filtering large datasets, or complex mathematical calculations, are ideal for web workers.

How do you terminate a web worker from the main thread?

To terminate a web worker, you can use the terminate() method from the main thread. This immediately stops the worker, ending its execution and freeing up any resources it was using.

Conclusion 

In this article, we have discussed the concept of multithreading in JavaScript using web workers. Web workers allow us to offload intensive tasks to separate threads, enabling parallel execution & improving application performance. With the help of web workers & leveraging asynchronous programming techniques, we can create responsive & efficient JavaScript applications. 

You can also practice coding questions commonly asked in interviews on Coding Ninjas Code360

Also, check out some of the Guided Paths on topics such as Data Structure and AlgorithmsCompetitive ProgrammingOperating SystemsComputer Networks, DBMSSystem Design, etc., as well as some Contests, Test Series, and Interview Experiences curated by top Industry Experts.

Live masterclass