Using web workers for safe, concurrent JavaScript

Web workers provide a way to run JavaScript code outside the single thread of execution in the browser. The single thread handles requests to display content as well as user interactions via keyboard, mouse clicks, and other devices, and also responses to AJAX requests.

Event handling and AJAX requests are asynchronous and can be considered a way to run some code outside the code path of general browser display, but they still run in this single thread and really have to finish fairly quickly.

Otherwise, interactivity in the browser stalls.

Web workers allow JavaScript code to run in a separate thread, entirely independent of the browser thread and its usual activities.

There’s been a lot of debate in recent years about what use there really is for web workers. CPUs are very fast these days, and almost everyone’s personal computer comes out of the box with several gigabytes of memory. Similarly, mobile devices have been approaching both the processor speed and memory size of desktop machines.

Applications that might once have been considered “computationally intensive” are now considered not so bad.

What do you mean this isn’t a vacuum?

But many times we consider only the execution of the one application, tested in the development environment, when deciding how to execute code efficiently. In a real-life system in the hands of a user, many things may be executing at once.

So applications that, running in isolation, might not have to use worker threads may have a valid need to use them to provide the best experience for a wide range of users.

Starting a new worker is as simple as specifying a file containing JavaScript code:

new Worker(‘worker-script.js’)

Once the worker is created, it is running in a separate thread independent of the main browser thread, executing whatever code is in the script that is given to it. The browser looks relative to the location of the current HTML page for the specified JavaScript file.

Data is passed between Workers and the main JavaScript thread using two complementary features in JavaScript code:

  • postMessage() function on the sending side
  • A message event handler on the receiving side

The message event handler receives an event argument, as other event handlers do; this event has a “data” property that has whatever data was passed from the other side.

This can be a two-way communication: the code in the main thread can call postMessage() to send a message to the worker, and the worker can send messages back to the main thread using an implementation of the postMessage() function that is available globally in the worker’s environment.

A very simple flow in a web worker would look like this: in the HTML of the page, a message is sent to the worker, and the page waits for a response:

var worker = new Worker("demo1-hello-world.js");

// Receive messages from postMessage() calls in the Worker
worker.onmessage = (evt) => {
    console.log("Message posted from webworker: " + evt.data);
}

// Pass data to the WebWorker
worker.postMessage({data: "123456789"});

The worker code waits for a message:

// demo1-hello-world.js
postMessage('Worker running');
onmessage = (evt) => {
    postMessage("Worker received data: " + JSON.stringify(evt.data));
};

The above code will print this to the console:

Message posted from webworker: Worker running
Message posted from webworker: Worker received data: {“data”:”123456789"}

Workers are expected to be long-lived, not stopped and started

Multiple messages can be sent and received between browser and worker during the life of a worker.

The implementation of web workers ensures safe, conflict-free execution in two ways:

  • A distinct, isolated global environment for the worker thread, separate from the browser environment
  • Pass-by-copy exchange of data between main and worker threads in the postMessage() call

Each worker thread has a distinct, isolated global environment that is different from the JavaScript environment of the browser page. Workers are not given any access at all to anything in the JavaScript environment of the page — not the DOM, nor the window or document objects.

Workers have their own versions of some things, like the console object for logging messages to the developer’s console, as well as the XMLHttpRequest object for making AJAX requests. But other than that, the JavaScript code that runs in a worker is expected to be self-contained; any output from the worker thread that the main window would want to use has to be passed back as data via the postMessage() function.

Furthermore, any data that is passed via postMessage() is copied before it is passed, so changing the data in the main window thread does not result in changes to the data in the worker thread. This provides inherent protection from conflicting concurrent changes to data that’s passed between main thread and worker thread.

Use cases for web workers

The typical use case for a web worker is any task that might become computationally expensive in the course of its execution, either by consuming a lot of CPU time or taking an unpredictably long amount of clock time to access data.

Some possible use cases for web workers:

  • Prefetching and/or caching data for later use
  • Polling and processing data from web services
  • Processing and display of large data sets (think genomics)
  • Computations related to moves in a game
  • Image processing and filtering
  • Processing text data (code syntax, spell checking, word count)

CPU time is the simple use case, but network access to resources can also be very important. Many times network communication over the internet can execute in milliseconds, but sometimes a network resource becomes unavailable, stalling until the network is restored or the request times out (which can take 1–2 minutes to clear).

And even if some code might not take very long to run when tested in isolation in the development environment, it could become an issue running in a user’s environment when multiple things could be running at the same time.

Leave a comment

Your email address will not be published. Required fields are marked *