Web workers and responsiveness

This post focuses on using HTML5 web workers for the purpose of keeping the user interface responsive while doing a lot of background computations. For other use cases I recommend to check the HTML Rock blog post on introducing web workers, or if you are interested in the deepest details, check the specification of web workers.

Why did I need a web worker

In a private project I created an application in which one of the modules needed data based on a computationally expensive task. The app is about graphical representation of the Mandelbrot set. As Wikipedia says

"The Mandelbrot set is the set of complex numbers 'c' for which the sequence ( c , c² + c , (c²+c)² + c , ((c²+c)²+c)² + c , (((c²+c)²+c)²+c)² + c , ...) does not approach infinity."

So this mathematical operation (we add the square of a complex number to the complex number, then we also do the same with the result of this operation) is iterated on a complex number again and again. The number is not in the set if this recursive operation results in approaching infinity. There are numbers for which you can tell after a few iteration if they approach infinity or not, but there are numbers, that need a lot iteration to figure out the same. As the number of iterations grows, it results more and more computing, and needs quite numerous processor time to calculate if a complex number is part of the set or not.

What happens during these computation with the user interface? It can be frozen, if you just wait until finishing of computing. This freezing is due to the fact, that javascript is single threaded in the context of browsers, so that it is not allowed doing more tasks in parallel at the same time like other programming languages in multi-threaded environments. And this is one of the scenarios where web workers come into play.

What is a web worker?

As Wikipedia says

A web worker [...] is a JavaScript script executed from an HTML page that runs in the background, independently of other user-interface scripts that may also have been executed from the same HTML page.

Web workers run in the background, they are not interrupted by user-interface scripts - and vice versa, these scripts don't hold up user interface scripts. They allows long tasks to be executed while keeping the UI responsive.

According to specification

There are two kinds of workers; dedicated workers, and shared workers. Dedicated workers, once created, are linked to their creator; but message ports can be used to communicate from a dedicated worker to multiple other browsing contexts or workers. Shared workers, on the other hand, are named, and once created any script running in the same origin can obtain a reference to that worker and communicate with it.

Here we will demonstrate how can you use dedicated workers for background computations.

Using web worker

We are going to run through the following steps:

  • isolate the computing task to a separate file
  • creating a worker in the main script
  • add event listeners
  • send messages to the worker
  • send messages by the worker to the main script.

First, we have to isolate the computing task to a separate file, that will be used by the worker. So we put our computing code to comp.js:

// comp.js  
  function calculateLots(n){
    var acc = 0;
    for (var i = 0; i < n; i++) {
      acc += i;
    }
    return acc;
  }

Next, we create a worker in the main script running in the window by invoking Worker() constructor specifying the URI of the script to execute in the worker thread:

// spawner.js  
  var worker = new Worker('comp.js');

We can start the worker by sending a message to this via worker.postMessage() method. But how does the worker get and process the message, and how does it send back? And last but not least, how does the main script process the message of the worker?

The scheme is that the worker has an event listener, that invokes a function with the message sent by worker.postMessage() from the main script. The worker processes the message, and sends data back to the main script via its self.postMessage() method. The main script also has an event listener, that calls a function with the message to process the data:

worker events

So we have to set up event listeners both in the worker and in the main script, and give the functions to run when the events occur:

// spawner.js  
    var webworker = {};

  (function(){
    webworker.setupWorker = function(){
      webworker.worker = new Worker('../src/comp.js');
      webworker.worker.addEventListener('message', messageProcessor, false);
      webworker.worker.postMessage(987654321);
      webworker.timer = new Date().getTime();
    };

    var messageProcessor = function(e){ // e is an event object
      var message = e.data;
      console.log(message); // 487730528962094400
      webworker.timer = new Date().getTime() - webworker.timer;
      console.log("The calculation took " + webworker.timer + " milliseconds");
      webworker.worker.terminate(); // we have to terminate the worker if
        // we don't need its services anymore
    };
  })();

and in comp.js:

// comp.js  
  self.addEventListener('message', calculateLots, false);

  function calculateLots(e){
    var message = e.data; // 987654321;
    var acc = 0;
    for (var i = 0; i < message; i++) {
      acc += i;
    }
    self.postMessage(acc);
  }
  

In the code above

  • the main script sends the number 987654321 to the worker as a message
  • it causes a message event in the worker context
  • the event handler of the worker gets the message, calculates sum of numbers from 1 to 987654321, and sends the result back to the main script
  • it causes a message event in main script context
  • the event handler of the main script gets the message and processes it.
  • we terminate the worker with the terminate() method from the main script (Workers may close themselves by calling self.close() method, too).

But what is that self? According to MDN the worker global scope is accessible through the self keyword, using the window instead of self (in order to get the current global scope) within a worker will return an error.

If you want to test the code above locally in Chrome, you will get a security issue:

Uncaught SecurityError: Failed to construct 'Worker': Script at 'file:///Users/tompascall/dev/playground/webworkers/src/comp.js' cannot be accessed from origin 'null'.

It is because for security reason a script is not allowed to access to your local file system in Chrome. If you'd like to test web workers locally, you have to open Chrome with a special flag.

On Mac:

open -a "Google Chrome" --args --allow-file-access-from-files

On Windows navigate to the library that contains chrome.exe, and launch chrome with the flag:

chrome.exe --args --allow-file-access-from-files

After that Chrome will be launched and you can test workers during this session.

In Firefox you have to override the security.fileuri.strict_origin_policy property setting in about:config to false. Make sure to set this back after you finished testing.

The message

What kinds of data can you pass to the worker?

As MDN says

Data passed between the main page and workers are copied, not shared. Objects are serialized as they're handed to the worker, and subsequently, de-serialized on the other end. The page and worker do not share the same instance, so the end result is that a duplicate is created on each end. Most browsers implement this feature as structured cloning.

The structured clone algorithm is a new algorithm defined by the HTML5 specification. It allows you to pass more complex types in and out of Workers such as RegExp, File, imageData, Blob, ArrayBuffer, and JSON objects. With the help of structured clone algorithm you can pass more kinds of data type than JSON, but it has its own limits, too, e.g. you cannot clone Error or Function objects.

If you want to send large data, you have an other option, passing data by transfer ownership (transferable objects). Transferable objects are transferred from one context to another with a zero-copy operation. Changing context means that the data in the calling context is no longer available once transferred to the new context and vice versa. For more information on transferable objects, performance and feature-detection for this method, see this HTML5 Rocks article.

Summarize

If you face the problem that you have a CPU intensive task which endangers the responsiveness of the user interface, you have HTML5 web workers as a quite useful tool to solve this situation. We demonstrated how to spawn a worker, and how to communicate with it. We showed the data types you can transfer to the worker, and methods of transferring.

Here you can test how web workers provide responsiveness during a computationally expensive task. You can check the project on Github.



comments powered by Disqus