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.
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):
Up to now, those problems were rarely
occurring for 2 main reasons:
- 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.
- 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:
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) {
var messageSent = event.data;
var messageReturned = "Hello " + messageSent + " from a separate thread!";
this.postMessage(messageReturned);
}
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( // 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:
- from the main calling page by
calling the
terminate()
command.
- 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( // 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:
function WorkerMessage(cmd, parameter) {
this.cmd = cmd;
this.parameter = parameter;
}
var _output = document.getElementById("output");
if (window.Worker) {
var _btnSubmit = document.getElementById("btnSubmit");
var _inputForWorker = document.getElementById("inputForWorker");
var _killWorker = document.getElementById("killWorker");
var myHelloWorker = new Worker('helloworkersJSON_EN.js');
myHelloWorker.addEventListener("message", function (event) {
_output.textContent = event.data;
}, false);
myHelloWorker.postMessage(new WorkerMessage('init', null));
_btnSubmit.addEventListener("click", function (event) {
myHelloWorker.postMessage(new WorkerMessage('hello', _inputForWorker.value));
}, false);
_killWorker.addEventListener("click", function (event) {
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:
if (window.Worker) {
}
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:
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:
Next step is to simply debug your worker
like you’re used to debugging your classic JavaScript code!
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);
By this one:
console.log.apply(console, args);
Then, you should be able to obtain such
results:
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:
- The initializing time and the
communication time with the worker shouldn’t be superior to the processing
itself
- The memory cost of using several Workers
- 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:
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:
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:
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:
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: