Introduction to Web Workers

TLDR

  • Web workers were created in 2009
  • Web workers are meant to be a general-purpose computation alternative so that you don’t overload the main thread of the browser

Why Web Workers?

Web workers are meant to be a general-purpose computation alternative so that you don’t overload the main thread of the browser. MDN has a one-line definition: “Web Workers are a simple means for web content to run scripts in background threads”. Demian Renzulli and Andrew Guan also dedicated a section of their work to explaining those differences.

As a simple analogy, web workers act as a thread would act in a programming language like Java. The main thread launches the web worker through the new operator and it will be created and wait for the task to be executed. From there, a relationship between the main script and the worker is created. Mariko Kosaka and Surma recorded a video that shows the worker power handling image processing.

What are Web Workers?

Web workers were created in 2009 and according to caniuse.com, they’re widely supported by modern browsers. In their early days, the proposed web workers were developed by a working group supported by w3c.

In the proposal, the authors divided web workers into two categories: web workers that run for a specific task (or as they’re called in the MDN Web Docs, dedicated workers), and shared workers that are available across pages and different pages can connect to them. In this tutorial, we’ll just focus on dedicated workers. There are a couple of similarities between web workers and service workers that we should mention:

  • They can run only from a server – trying to access them from the file system won’t work.
  • Accessing the Document Object Model (DOM) is not allowed.

Dedicated workers

Let’s see how it works with an example. First, we define a script that will be the worker. Later on, we invoke the worker passing the script as the first argument in the constructor, such as:

const myWorker = new Worker('src/script.js');

Another way of launching the worker is through a raw string with source code that is then passed as a Blob. With this approach you don’t need to declare the worker in a separate file.

Besides that, it is also possible to use the tag script with the type “javascript/worker”, but both approaches are not widely shared in tutorials across the internet. Green calls this approach an “inline worker”.

const code = 'any javascript code here';
const myWorker = new Worker(URL.createObjectURL(new Blob([code])));

With the worker created, we can start to communicate with it. The communication with the web workers happens via events, as any other Application Programming Interface available in the Document Object Model would. To react to any event from the main thread, we listen to the message event. To communicate back to the parent who created the worker, we use postMessage.

The following example is taken from the web worker specification. It depicts a simple example of listening to the worker’s computation. Note that in the code example that follows, we use onmessage. This is due to its API. Using message would require us to use addEventListener instead.

var n = 1;
search: while (true) {
  n += 1;
  for (var i = 2; i <= Math.sqrt(n); i += 1)
    if (n % i == 0)
     continue search;
  // found a prime!
  postMessage(n);
}

Then, in an index.html file:

<!DOCTYPE HTML>
<html lang="en">
 <head>
  <meta charset="utf-8"/>
  <title>Worker example: One-core computation</title>
 </head>
 <body>
  <p>The highest prime number discovered so far is: <output id="result"></output></p>
  <script>
   var worker = new Worker('worker.js');
   worker.onmessage = function (event) {
     document.getElementById('result').textContent = event.data;
   };
  </script>
 </body>
</html>

You can also terminate workers. This frees up resources that are allocated in the browser. Terminating a worker means killing its execution, and the communication between the main thread and the worker is removed. To establish it back, the worker needs to be created again.

There are two ways of terminating a worker: first, it can be called from the main thread. In this example, it is the script in the index.html. You can also terminate it from inside the worker (StackOverflow has a question that dives into this subject). From the main thread, terminating a worker would be something like this:

<!DOCTYPE HTML>
<html lang="en">
 <head>
  <meta charset="utf-8" />
  <title>Worker example: One-core computation</title>
 </head>
 <body>
  <p>The highest prime number discovered so far is: <output id="result"></output></p>
  <script>
   var worker = new Worker('worker.js');
   worker.onmessage = function (event) {
     document.getElementById('result').textContent = event.data;
     worker.terminate();
   };
  </script>
 </body>
</html>

From the worker.js, you don’t need the worker.terminate, you can call close directly. The message to the parent will be posted and then the worker will be terminated immediately:

var n = 1;
search while (true) {
  n += 1;
  for (var i = 2; i <= Math.sqrt(n); i += 1)
    if (n % i == 0)
     continue search;
  // found a prime!
  postMessage(n);
  self.close();
}

You don’t need to terminate the worker after its execution, but in the absence of it, resources will be consumed. Figure 1 below depicts this scenario (to inspect what is going on with the worker, the tab “Sources” is used). On the left, you can see the list of workers created. Those were created when the user clicked on the button “click me worker”.

Clicking on the button once creates one worker, and the button will create as many workers as times it’s clicked. For further inspection, you can look at the source code on GitHub.

Figure 1: Web worker with multiple instances running, if no close function is invoked, the worker instance keeps listening to events consuming resources. Figure 1: Web worker with multiple instances running, if no close function is invoked, the worker instance keeps listening to events consuming resources.

You can create as many workers as you want – the web workers specification does not force any hard limit. But the resources in the real device are limited as well, so take care not to create too many.

Figure 2: Once the worker finishes its job it can invoke the close function to terminate its execution. Figure 2: Once the worker finishes its job it can invoke the close function to terminate its execution.

Figure 2 shows that the worker closes itself once the computation is done. On the left, you can see only one.

With this approach the workers will still be created (as many times as the user clicks) – but the difference is that each one will terminate itself after communicating to the main thread that the work is done. Lastly, we need to discuss the execution model that changes when a worker is implemented. With a worker, a new actor is in place, and the code that used to run synchronously with the script now delegates that to the worker and waits for its response.

There is a cost associated with creating and disposing of workers. The life cycle is more complex if we compare that with the code execution without it. In the next section, I’ll introduce you to the JSON tool so you can understand what it does and what problems it solves.

Web workers use cases

  • [How to Work with JSON Strings without Blocking User Interactions]/2023/05/10/web-workers-to-the-rescue-how-to-work-with-json-strings-without-blocking-user-interactions.html