Thursday, April 25, 2013

Make Your UI More Responsive with HTML5 Web Workers

Argh!!... Your web application has to sort a lot of data and you get the dreaded error message... "A script on this page may be busy, or it may have stopped responding..." Or maybe you're writing some image processing code that takes forever on large images... Your UI is non-responsive. What are you going to do!!?... You could try to break up the work into small pieces and use timers to spread out the work. But what a PITA!

What if JavaScript had the ability to run your code in the background and not interfere with the responsiveness of your UI. What if you could spawn a thread to do long running computations such as image filtering and sorting. Well with HTML5 Web Workers you can do just that! In the rest of this article, I'm going to teach you how to use Web Workers within your own applications.

Web Workers are an HTML5 feature that allow a JavaScript developer to create additional threads of execution which can be used to execute long running code in the background. Normally your JavaScript runs on what is called the main UI thread. Everything you do from JavaScript including all DOM manipulations are executed sequentially on this single thread. If your JavaScript code runs too long without returning control to the browser, your UI will stop responding to the user. The following single line of code shows how to create a web worker:

// A webworker is created and the JavaScript file longjob.js
// is loaded into its context.
worker = new Worker("longjob.js");

This code creates a new web worker object that has its own JavasScript context running in its own thread, loads the referenced JavaScript URL into that context and executes it. In the next code block, we'll walk through this again giving a bit more context.

var worker;
function runInWebWorker() {
// Does our browser support webworkers?
if (!worker && window.Worker) {
// A webworker is created and the JavaScript file longjob.js
// is loaded into its context.
worker = new Worker("longjob.js");
if (worker) {
// Register an event handler that will receive messages from our
// web worker
worker.addEventListener('message', function(e) {
// Our webworker sends us a message when it is done.
alert("longJob returned: " + e.data);
});
}
// Send a message to the worker to execute the longJob function
// in the context of the workers thread.
worker.postMessage(30);
}
else alert("Web Workers are not Supported in this Browser.");
}

Message passing is the mechanism used to communicate between a web worker and the main UI thread. The web worker's postMessage method is used to send messages to and from the web worker. In order to receive a message an event handler must be registered with the addEventListener method. In the code block above, after checking to see if the browser supports web workers and we've created the web worker, a message event handler is registered on the web worker object. This particular event handler is used by the main UI thread to receive messages from our web worker. Next a call to vpostMessage is made to send a message in the other direction to our web worker. In the sample, we pass a simple numeric value of 30 as the message payload (which will be the number of seconds that we want our job to run).

Note: You can send more complicated messages in the form of arrays or simple objects. In the next code block, we'll look at the code loaded into our web worker, the contents of longjob.js:

// longjob.js
// function to simulate a long running job
// loops for approximately n seconds.
function longJob(n) {
var start = new Date();
var elapsedSeconds = 0;
while(elapsedSeconds < n) {
var v = 0;
for (var i = 0; i < 1000000; i++) v += i;
var end = new Date();
elapsedSeconds = (end.getTime() - start.getTime())/1000.0;
}
return "All Done in " + elapsedSeconds + " seconds.";
}
// Our webworker registers for an message event so we can talk
// to it from our main thread and ask it to do something.
self.addEventListener('message', function(e) {
// Invoke the longjob function within our worker thread
// and pass in the parameter that was sent in.
var result = longJob(e.data);
// Send a message back to the main thread with the result
self.postMessage(result);
}, false);

The longJob function simulates a long running function by taking a numeric value n and looping until n seconds have transpired. Next an event handler is registered with a call to self.addEventListener so that our web worker can receive messages sent to it from the main UI thread. Note that within the context of our web worker the special variables this and self reference the web worker object itself. The parameter passed into the postMessageAPI is made available as a data property (shown as e.data in code above) on the event object passed into the event callback function. When longJob completes our event handler sends a message back to the main thread using the postMessage method.

Web workers can be a very useful and empowering feature but they do have some limitations. All communication into and out of your web workers is done by passing in and out data in the form of messages. All data is passed by value meaning that any object references are converted to a serialized copy of the objects data. You can't directly access or manipulate the DOM from a web worker. While this may seem somewhat limiting these restrictions pretty much eliminate all of the synchronization bugs that can arise when using threads.