Wednesday, October 9, 2024

Introduction to HTML5 Web Workers: the JavaScript Multi-threading Approach

written by David Rousset

HTML5 applications are obviously written using JavaScript. But compared to other kinds of development environments (like native ones), JavaScript historically suffers from an important limitation: all of its execution processe remain inside a unique thread.

This could be pretty annoying with today’s multi-core processors like the i5/i7 containing up to 8 logical CPUs—and even with the latest ARM mobile processors being dual or even quad-cores. Hopefully, we’re going to see that HTML5 offers the Web a better way to handle these new, marvelous processors to help you embrace a new generation of Web applications.

Image 1s

Before the Workers…

This JavaScript limitation implies that a long-running process will freeze the main window. We often say that we’re blocking the “UI Thread”. This is the main thread in charge of handling all the visual elements and associated tasks: drawing, refreshing, animating, user inputs events, etc.

We all know the bad consequences of overloading this thread: the page freezes and the user can’t interact with your application any more. The user experience is then, of course, very unpleasant, and the user will probably decide to kill the tab or the browser instance. Probably not something you’d like to see happen to your app!

To avoid that, browsers have implemented a protection mechanism which alerts users when a long-running suspect script occurs.
Unfortunately, this mechanism can’t tell the difference between a script not written correctly and a script that just needs more time to accomplish its work. Still, as it blocks the UI thread, it’s better to tell you that something wrong is maybe currently occurring. Here are some message examples (from Firefox 5 & IE9):

Image 2

Up to now, those problems were rarely occurring for 2 main reasons:

  1. HTML and JavaScript weren’t used in the same way and for the same goals as other technologies able to achieve multi-threaded tasks. The Websites were offering richless experiences to the users compared to native applications.
  2. There were some other ways to more or less solve this concurrency problem.

Those ways are well-known to all Web developers. For instance, we were trying to simulate parallel tasks thanks to the setTimeout() and setInterval() methods. HTTP requests can also be done in an asynchronous manner, thanks to the XMLHttpRequest object that avoids freezing the UI while loading resources from remote servers. At last, the DOM Events let us write applications giving the illusion that several things occur at the same time. Illusion, really? Yes!

To better understand why, let’s have a look at a fake piece of code and see what happens inside the browser:

<script type=”text/javascript”>
    function init(){
        { piece of code taking 5ms to be executed }
        A mouseClickEvent is raised
        { piece of code taking 5ms to be executed }
        setInterval(timerTask,”10″);
        { piece of code taking 5ms to be executed }
    }

    function handleMouseClick(){
          piece of code taking 8ms to be executed
    }

    function timerTask(){
          piece of code taking 2ms to be executed
    }
</script>

Let’s take this code to project it on a model. This diagram shows us what’s happening in the browser on a time scale:

Image 3

This diagram well-illustrates the non-parallel nature of our tasks. Indeed, the browser is only enqueuing the various execution requests:

– from 0 to 5ms: the init() function starts by a 5ms task. After 5ms, the user raises a mouse click event. However, this event can’t be handled right now as we’re still executing the init() function which currently monopolizes the main thread. The click event is saved and will be handled later on.

– from 5 to 10ms: the init() function continues its processing during 5ms and then asks to schedule the call to the timerTask() in 10ms. This function should then logically be executed at the 20ms timeframe.

– from 10 to 15ms: 5 new milliseconds are needed to finish the complete run of the init() function. This is then corresponding to the 15ms yellow block. As we’re freeing the main thread, it can now start to dequeue the saved requests.

– from 15 to 23ms: the browser starts by running the handleMouseClock() event which runs during 8ms (the blue block).

– from 23 to 25 ms: as a side effect, the timerTask() function which was scheduled to be run on the 20ms timeframe is slightly shifted of 3ms. The other scheduled frames (30ms, 40ms, etc.) are respected as there is no more code taking some CPU.

Note: This sample and the above diagram (in SVG or PNG via a feature detection mechanism) were inspired by the following article:

HTML5 Web Workers Multithreading in JavaScript

All these tips don’t really solve our initial problem: everything keeps being executed inside the main UI thread.

Plus, even if JavaScript hasn’t been used for the same types of applications like the “high-level languages,” it starts to change with the new possibilities offered by HTML5 and its friends. It’s then more important to provide to JavaScript with some new powers to make it ready to build a new generation of applications capable of leveraging parallel tasks. This is exactly what the Web Workers were made for.

Web Workers or How to Be Executed Out of the UI Thread

The Web Workers APIs define a way to run script in the background. You can then execute some tasks in threads living outside the main page and thus non-impacting the drawing performance. However, in the same way that we know that not all algorithms can be parallelized, not all JavaScript code can take advantage of Workers. Ok, enough blah blah blah, let’s have a look at those famous Workers.

My 1st Web Worker

As Web Workers will be executed on separated threads, you need to host their code into separated files from the main page. Once done, you need to instantiate a Worker object to call them:

var myHelloWorker = new Worker(‘helloworkers.js’);

You’ll then start the worker (and thus a thread under Windows) by sending it a first message:

myHelloWorker.postMessage();

Indeed, the Web Workers and the main page are communicating via messages. Those messages can be formed with normal strings or JSON objects. To illustrate simple message posting, we’re going to start by reviewing a very basic sample. It will post a string to a worker that will simply concatenate it with something else. To do that, add the following code into the “helloworker.js” file:

function messageHandler(event) {
    // Accessing to the message data sent by the main page
    var messageSent = event.data;
    // Preparing the message that we will send back
    var messageReturned = “Hello ” + messageSent + ” from a separate thread!”;
    // Posting back the message to the main page
    this.postMessage(messageReturned);
}

// Defining the callback function raised when the main page will call us
this.addEventListener(‘message’, messageHandler, false);

We’ve just defined inside “helloworkers.js” a piece of code that will be executed on another thread. It can receive messages from your main page, do some tasks on it, and send a message back to your page in return. Then we need to write the receiver in the main page. Here is the page that will handle that:

<!DOCTYPE html>
<html>
<head>
    <title>Hello Web Workers</title>
</head>
<body>
    <div id=”output”></div>

    <script type=”text/javascript”>
        // Instantiating the Worker
        var myHelloWorker = new Worker(‘helloworkers.js’);
        // Getting ready to handle the message sent back
        // by the worker
        myHelloWorker.addEventListener(“message”, function (event) {
            document.getElementById(“output”).textContent = event.data;
        }, false);

        // Starting the worker by sending a first message
        myHelloWorker.postMessage(“David”);

        // Stopping the worker via the terminate() command
        myHelloWorker.terminate();
    </script>
</body>
</html>

The result will be: “Hello David from a separate thread!” You’re impressed, aren’t you?

Be aware that the worker will live until you kill it.

Since they aren’t automatically garbage collected, it’s up to you to control their states. And keep in mind that instantiating a worker will cost some memory…and don’t neglect the cold start time either. To stop a worker, there are 2 possible solutions:

  1. from the main calling page by calling the terminate() command.
  2. from the worker itself via the close() command.

DEMO: You can test this slightly enhanced sample in your browser.

Posting messages using JSON

Of course, most of the time we will send more structurated data to the Workers. (By the way, Web Workers can also communicate between each other using Message channels.)

But the only way to send structurated messages to a worker is to use the JSON format. Luckily, browsers that currently support Web Workers are nice enough to also natively support JSON. How kind they are!

Let’s take our previous code sample. We’re going to add an object of type WorkerMessage. This type will be used to send some commands with parameters to our Web Workers.

Let’s use the following simplified HelloWebWorkersJSON_EN.htm Web page

<!DOCTYPE html>
<html>
<head>
    <title>Hello Web Workers JSON</title>
</head>
<body>
    <input id=inputForWorker /><button id=btnSubmit>Send to the worker</button><button id=killWorker>Stop the worker</button>
    <div id=”output”></div>

    <script src=”HelloWebWorkersJSON.js” type=”text/javascript”></script>
</body>

</html>

We’re using the Unobtrusive JavaScript approach which helps us dissociate the view from the attached logic. The attached logic is then living inside this HelloWebWorkersJSON_EN.js file:

// HelloWebWorkersJSON_EN.js associated to HelloWebWorkersJSON_EN.htm

// Our WorkerMessage object will be automatically
// serialized and de-serialized by the native JSON parser
function WorkerMessage(cmd, parameter) {
    this.cmd = cmd;
    this.parameter = parameter;
}

// Output div where the messages sent back by the worker will be displayed
var _output = document.getElementById(“output”);

/* Checking if Web Workers are supported by the browser */
if (window.Worker) {
    // Getting references to the 3 other HTML elements
    var _btnSubmit = document.getElementById(“btnSubmit”);
    var _inputForWorker = document.getElementById(“inputForWorker”);
    var _killWorker = document.getElementById(“killWorker”);

    // Instantiating the Worker
    var myHelloWorker = new Worker(‘helloworkersJSON_EN.js’);
    // Getting ready to handle the message sent back
    // by the worker
    myHelloWorker.addEventListener(“message”, function (event) {
        _output.textContent = event.data;
    }, false);

    // Starting the worker by sending it the ‘init’ command
    myHelloWorker.postMessage(new WorkerMessage(‘init’, null));

    // Adding the OnClick event to the Submit button
    // which will send some messages to the worker
    _btnSubmit.addEventListener(“click”, function (event) {
        // We’re now sending messages via the ‘hello’ command
        myHelloWorker.postMessage(new WorkerMessage(‘hello’, _inputForWorker.value));
    }, false);

    // Adding the OnClick event to the Kill button
    // which will stop the worker. It won’t be usable anymore after that.
    _killWorker.addEventListener(“click”, function (event) {
        // Stopping the worker via the terminate() command
        myHelloWorker.terminate();
        _output.textContent = “The worker has been stopped.”;
    }, false);
}
else {
    _output.innerHTML = “Web Workers are not supported by your browser. Try with IE10: <a href=\”http://ie.microsoft.com/testdrive\”>download the latest IE10 Platform Preview</a>”;
}

At last, here is the code for the Web Worker contained in helloworkerJSON_EN.js the file:

function messageHandler(event) {
    // Accessing to the message data sent by the main page
    var messageSent = event.data;

    // Testing the command sent by the main page
    switch (messageSent.cmd) {
        case ‘init’:
            // You can initialize here some of your models/objects
            // that will be used later on in the worker (but pay attention to the scope!)
            break;
        case ‘hello’:
            // Preparing the message that we will send back
            var messageReturned = “Hello ” + messageSent.parameter + ” from a separate thread!”;
            // Posting back the message to the main page
            this.postMessage(messageReturned);
            break;
    }
}

// Defining the callback function raised when the main page will call us
this.addEventListener(‘message’, messageHandler, false);

Once again, this sample is very basic. Still, it should help you to understand the underlying logic. For instance, nothing prevents you from using the same approach to send some gaming elements that will be handled by an AI or physics engine.

DEMO: You can test this JSON sample here.

This article was reprinted with permission from Microsoft Corporation. This site does business with Microsoft Corporation.

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Popular Articles

Featured