Click here to Skip to main content
Click here to Skip to main content

Introduction to HTML5 Web Workers: The JavaScript Multi-Threading Approach

, 3 Apr 2012
In this article 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

Editorial Note

This article is in the Product Showcase section for our sponsors at CodeProject. These reviews are intended to provide you with information on products and services that we consider useful and of value to developers.

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 its execution process remains 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.

HTML5-Web-Workers/image001.jpg

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):

HTML5-Web-Workers/image002.jpg

HTML5-Web-Workers/image003.jpg

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:

HTML5-Web-Workers/image004.jpg

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 negligate 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 here: http://david.blob.core.windows.net/html5/HelloWebWorkers_EN.htm

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</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>

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>";
}

Once again, this sample is very basic. Still, it should help you to understand the underlying logic. For instance, nothing prevents you to use 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: http://david.blob.core.windows.net/html5/HelloWebWorkersJSON_EN.htm

Browsers Support

Web Workers have just arrived in the IE10 Platform Preview. This is also supported by Firefox (since 3.6), Safari (since 4.0), Chrome & Opera 11. However, this is not supported by the mobile versions of these browsers. If you’d like to have a more detailed support matrix, have a look here: http://caniuse.com/#search=worker

In order to dynamically know that this feature is supported in your code, please use the feature detection mechanism. (You shouldn’t use some user-agent sniffing!)

To help you, there are 2 available solutions. The first one is to simply test the feature yourself using this very simple piece of code:

/* Checking if Web Workers are supported by the browser */
if (window.Worker) {
    // Code using the Web Workers
}

The second one is to use the famous Modernizr library (now natively shipped with the ASP.NET MVC3 project templates). Then, simply use a code like that:

<script type="text/javascript">
    var divWebWorker = document.getElementById("webWorkers");
    if (Modernizr.webworkers) {
        divWebWorker.innerHTML = "Web Workers ARE supported";
    }
    else {
        divWebWorker.innerHTML = "Web Workers ARE NOT supported";
    }
</script>

Here, for instance, is the current support in your browser: Web Workers are not supported inside your browser.

This will allow you to expose 2 versions of your application. If Web Workers are not supported, you will simply execute your JavaScript code as usual. If Web Workers are supported, you will be able to push some of the JavaScript code to the workers to enhance the performance of your applications for the most recent browsers. You won’t then break anything or build a specific version only for the very latest browsers. It will work for all browsers with some performance differences.

Non-Accessible Elements from a Worker

Rather than looking at what you don’t have access to from Workers, let’s take a look at what you only have access to:

Method Description
void close(); Terminates the worker thread.
void importScripts(urls); A comma-separated list of additional JavaScript files.
void postMessage(data); Sends a message to or from the worker thread.

 

Attributes Type Description
location WorkerLocation Represents an absolute URL, including protocol, host, port, hostname, pathname, search, and hash components.
navigator WorkerNavigator Represents the identity and onLine state of the user agent client.
self WorkerGlobalScope The worker scope, which includes the WorkerLocation and WorkerNavigator objects.

 

Event Description
onerror A runtime error occurred.
onmessage Message data received.

 

Method Description
void clearInterval(handle); Cancels a timeout identified by handle.
void clearTimeout(handle); Cancels a timeout identified by handle.
long setInterval(handler, timeout value, arguments); Schedules a timeout to be run repeatedly after the specified number of milliseconds. Note that you can now pass additional arguments directly to the handler. If handler is a DOMString, it is compiled as JavaScript. Returns a handle to the timeout. Clear with clearInterval.
long setTimeout(handler, timeout value, arguments); Schedules a timeout to run after the specified number of milliseconds. Note that you can now pass additional arguments directly to the handler. If handler is a DOMString, it is compiled as JavaScript. Returns a handle to the timeout. Clear with clearTimeout.

Note: This table is extracted from our MSDN documentation: HTML5 Web Worker

In summary, you don’t have access to the DOM. Here is a very good diagram summarizing that:

HTML5-Web-Workers/image007.jpg

For instance, since you don’t have access to the window object from a worker, you won’t be able to access the Local Storage (which doesn’t seem to be thread-safe anyway). Those limitations may look too constraint for developers used to multi-threaded operations in other environments. However, the big advantage is we won’t fall into the same problems we usually encounter: lock, races conditions, etc. We won’t have to think about that with Web Workers. This makes the Web Workers something very accessible, while allowing some interesting performance boosts in specific scenarios.

Error Handling & Debugging

It is very easy to handle errors raised from your Web Workers. You simply have to subscribe to the OnError event in the same way we’ve done it with the OnMessage event:

myWorker.addEventListener("error", function (event) {
    _output.textContent = event.data;
}, false);

This is the best Web Workers can give you natively to help you debugging their code… This is very limited, isn’t it?

The F12 Development Bar for a Better Debugging Experience

To go beyond that, IE10 offers you to directly debug the code of your Web Workers inside its script debugger like any other script.

For that, you need to launch the development bar via the F12 key and navigate to the "Script” tab. You shouldn’t see the JS file associated to your worker yet. But right after pressing the "Start debugging” button, it should magically be displayed:

HTML5-Web-Workers/image008-1.jpg

Next step is to simply debug your worker like you’re used to debugging your classic JavaScript code!

HTML5-Web-Workers/image009-1.jpg

IE10 is currently the only browser offering you that. If you want to know more about this feature, you can read this detailed article: Debugging Web Workers in IE10

An Interesting Solution to Mimic console.log()

At last, you need to know that the console object is not available within a worker. Thus, if you need to trace what’s going on inside the worker via the .log() method, it won’t work as the console object won’t be defined. Hopefully, I’ve found an interesting sample that mimics the console.log() behavior by using the MessageChannel: console.log() for Web Workers. This works well inside IE10, Chrome & Opera but not in Firefox as it doesn’t support the MessageChannel yet.

Note: In order to make the sample from this link work in IE10, you need to change this line of code:

console.log.apply(console,
args); // Pass the args to the real log

By this one:

console.log.apply(console, args); // Pass the args to the real log

Then, you should be able to obtain such results:

HTML5-Web-Workers/image010.jpg

DEMO: If you want to try this console.log() simulation, navigate here -> http://david.blob.core.windows.net/html5/HelloWebWorkersJSONdebug.htm <-

Use Cases and How to Identify Potential Candidates

Web Workers for which scenarios?

When you browse the Web looking for sample usages of the Web Workers, you always find the same kind of demos: intensive mathematical/scientific computation. You’ll then find some JavaScript raytracers, fractals, prime numbers, and stuff like that. Nice demos to understand the way Workers works, but this gives us few concrete perspectives on how to use them in "real world” applications.

It’s true that the limitations we’ve seen above on the resources available inside Web Workers narrow down the number of interesting scenarios. Still, if you just take some time to think about it, you’ll start to see new interesting usages:

  • image processing by using the data extracted from the <canvas> or the <video> elements. You can divide the image into several zones and push them to the different Workers that will work in parallel. You’ll then benefit from the new generation of multi-cores CPUs. The more you have, the faster you’ll go.
  • big amount of data retrieved that you need to parse after an XMLHTTPRequest call. If the time needed to process this data is important, you’d better do it in background inside a Web Worker to avoid freezing the UI Thread. You’ll then keep a reactive application.
  • background text analysis: as we have potentially more CPU time available when using the Web Workers, we can now think about new scenarios in JavaScript. For instance, we could imagine parsing in real-time what the user is currently typing without impacting the UI experience. Think about an application like Word (of our Office Web Apps suite) leveraging such possibility: background search in dictionaries to help the user while typing, automatic correction, etc.
  • concurrent requests against a local database. IndexDB will allow what the Local Storage can’t offer us: a thread-safe storage environment for our Web Workers.

Moreover, if you switch to the video game world, you can think about pushing the AI or physics engines to the Web Workers. For instance, I’ve found this experimentation: On Web Workers, GWT, and a New Physics Demo which use the Box2D physic engine with Workers. For your Artificial Intelligence engine, this means also that you will be able in the same timeframe to process more data (anticipate more moves in a chess game for instance).

Some of my colleagues may now argue that the only limit is your imagination!

But in a general manner, as long as you don’t need the DOM, any time-consuming JavaScript code that may impact the user experience is a good candidate for the Web Workers. However, you need to pay attention to 3 points while using the Workers:

  1. The initializing time and the communication time with the worker shouldn’t be superior to the processing itself
  2. The memory cost of using several Workers
  3. The dependency of the code blocks between them as you may then need some synchronization logic. Parallelization is not something easy my friends!

On our side, we’ve recently published the demo named Web Workers Fountains:

HTML5-Web-Workers/image011.jpg

This demo displays some particles effects (the fountains) and uses 1 Web Worker per fountain to try to compute the particles in the fastest way possible. Each Worker result is then aggregated to be displayed inside the <canvas> element. Web Workers can also exchange messages between them via the Message Channels. In this demo, this is used to ask to each of the Workers when to change the color of the fountains. We’re then looping through this array of colors: red, orange, yellow, green, blue, purple, and pink, thanks to the Message Channels. If you’re interested in the details, jump into the LightManager() function of the Demo3.js file.

Also, feel free to launch this demo inside Internet Explorer 10, it’s fun to play with!

How to identify hot spots in your code

To track the bottlenecks and identify which parts of your code you could send to the Web Workers, you can use the script profiler available with the F12 bar of IE9/10. It will then help you to identify your hot spots. However, identifying a hot spot doesn’t mean you’ve identified a good candidate for Web Workers. To better understand that, let’s review together two different interesting cases.

Case 1: Animation inside <canvas> with the Speed Reading demo

This demo comes from IE Test Drive and can be browsed directly here: Speed Reading. It tries to display as fast as possible some characters using the <canvas> element. The goal is to stress the quality of the implementation of the hardware acceleration layer of your browser. But going beyond that, would it be possible to obtain more performance by splitting some operations on threads? We need to achieve some analysis to check that.

If you run this demo inside IE9/10, you can also start the profiler within a couple of seconds. Here is the kind of results you’ll obtain:

HTML5-Web-Workers/image013.jpg

If you’re sorting the time-consuming functions in decreasing order, you’ll clearly see those functions coming first: DrawLoop(), Draw() and drawImage(). If you’re double-clicking on the Draw line, you’ll jump into the code of this method. You’ll then observe several calls of this type:

surface.drawImage(imgTile, 0, 0, 70, 100, this.left, this.top, this.width, this.height);

Where the surface object is referencing a <canvas> element.

A quick conclusion of this brief analysis is that this demo spends most of its time drawing inside the Canvas through the drawImage() method. As the <canvas> element is not accessible from a Web Worker, we won’t be able to offload this time-consuming task to different threads (we could have imagined some ways of handling the <canvas> element in a concurrency manner for instance). This demo is then not a good candidate for the parallelization possibilities offered by the Web Workers.

But it’s well-illustrating the process you need to put in place. If, after some profiling job, you’re discovering that the major part of the time-consuming scripts are deeply linked to DOM objects, the Web Workers won’t be able to help you boost the performance of your Web app.

Case 2: Raytracers inside <canvas>

Let’s now take another easy example to understand. Let’s take a raytracer like this one: Flog.RayTracer Canvas Demo. A raytracer uses some very CPU-intensive mathematical computations in order to simulate the path of light. The idea is to simulate some effects like reflection, refraction, materials, etc.

Let’s render a scene while launching the script profiler. You should obtain something like this:

HTML5-Web-Workers/image014.jpg

Again, if we sort the functions in decreasing order, 2 functions clearly seem to take most of the time: renderScene() and getPixelColor().

The goal of the getPixelColor() method is to compute the current pixel. Indeed, ray-tracing is rendering a scene pixel per pixel. This getPixelColor() method is then calling the rayTrace() method in charge of rendering the shadows, ambient light, etc. This is the core of our application. And if you’re reviewing the code of the rayTrace() function, you’ll see that it’s 100% pure JavaScript juice. This code has no DOM dependency. Well, I think you’ll get it: this sample is a very good candidate to parallelization. Moreover, we can easily split the image rendering on several threads (and thus potentially on several CPUs) as there’s no synchronization needed between each pixel computation. Each pixel operation is independent from its neighborhood as no anti-aliasing is used in this demo.

This is then not a surprise if we can find some raytracers samples using some Web Workers like this one: http://nerget.com/rayjs-mt/rayjs.html

After profiling this raytracer using IE10, we can see the important differences between using no Worker and using 4 Workers:

HTML5-Web-Workers/image015.jpg

In the first screenshot, the processRenderCommand() method is using almost all of the CPU available and the scene is rendered in 2.854s.

With 4 Web Workers, the processRenderCommand() method is executed in parallel on 4 different threads. We can even see their Worker Id on the right column. The scene is rendered this time in 1.473s. The benefits were real: the scene has been rendered 2 times faster.

Conclusion

There is no magical or new concept linked to the Web Workers in the way to review/architect your JavaScript code for parallel execution. You need to isolate the intensive part of your code. It needs to be relatively independent of the rest of your page’s logic to avoid waiting for synchronization tasks. And the most important part: the code shouldn’t be linked to the DOM. If all these conditions are met, think about the Web Workers. They could definitely help you boost the general performance of your Web app!

Additional Resources

Here are some interesting additional resources to read:

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

David Rousset

United States United States
No Biography provided

Comments and Discussions

 
Questionrunning demo on localhost Pinmembermikeh73-Jun-12 8:19 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web04 | 2.8.140827.1 | Last Updated 3 Apr 2012
Article Copyright 2011 by David Rousset
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid