Monday, August 2, 2010

An Introduction to HTML 5 Web Workers


Prior to HTML 5 Web Workers, all JavaScript code would run in the same thread as the UI of that browser window. The result was that all long-running scripts would cause the browser window to freeze until processing finished. If the script took long enough, the browser would prompt the user to see if he/she wanted to stop the unresponsive script.

Web workers allow for script to run in a separate thread from the browser window's UI. This allows long-running scripts to take place without interfering with the browser's user interface (the window stays responsive to input, like clicks and scrolling, even while processing).

As a result of running in a separate thread from the browser's UI the worker thread does not have direct access to the DOM or other UI objects.

If the worker thread needs the UI updated based on processing, the JavaScript that created the worker thread can set up a callback function to listen for messages from the worker. The main UI thread could then respond to the messages from the worker and do the proper UI modification.

One thing to be aware of with web workers is that they are not intended to be used in large numbers and are expected to be long-lived. The worker threads also have a high start-up performance cost as well as a high memory cost per worker instance.


Types of Web Workers

There are two types of web workers available: Dedicated Workers and Shared Workers.

Dedicated workers are linked to their creator. They are also a bit simpler to create and work with than shared workers.

Shared workers allow any script from the same origin/domain to obtain a reference to the worker and communicate with it.


Browser Support

Dedicated Web Workers are supported in Firefox 3.5+, Chrome 5+, and Safari 5+ (I've read that dedicated web workers are supported in Chrome 4 and Safari 4 as well but I haven't tested those browsers to know for sure).

Shared Web Workers are supported in Chrome 5.0+ and Safari 5.0+. I haven't tested Shared Web Worker support in Chrome 4 or Safari 4 so I don't know if those browsers support it or not.

Firefox does not appear to support Shared Web Workers (tested in 3.6.8 as well as 4.0 beta 2).

Internet Explorer does not yet support either type of web worker (tested in IE 8 as well as IE 9 Preview 3).


Test for Browser Features

Because web workers are a relatively new technology, not all browsers will support them. Even in a browser that supports one type of web worker, it might not support the other type (Firefox, for example, currently only supports dedicated web workers).

It is always best to test if a browser supports a particular feature before trying to use it.

The following is an example of how you would test if a browser supports Dedicated Web Workers or Shared Web Workers:

// Test if Dedicated Web Workers are available
if (window.Worker) { g_bDedicatedWorkersEnabled = true; }

// Test if Shared Web Workers are available
if (window.SharedWorker) { g_bSharedWorkersEnabled = true; }


Dedicated Web Workers

To create a dedicated web worker, you simply create a new instance of the Worker object passing in a string that specifies the location of the JavaScript file for the worker code. The following is an example of creating a dedicated worker instance:

var aWorker = new Worker("DedicatedWebWorker.js");

The worker object was designed to accept a string (location of the JavaScript file) rather than a function in order to prevent a developer from creating a closure that would allow the worker thread to gain direct access to the browser's DOM or other page elements.

Communication between the creator of the worker thread and the worker thread itself is achieved by means of postMessage calls. To pass a message to the worker thread you would do the following:

aWorker.postMessage("Hello from the main page");

In order for the creator of the worker thread to receive messages from the worker thread, you will need to set up a function to receive the 'onmessage' event. The following is an example of how you would attach a function to the onmessage event:

aWorker.onmessage = OnWorkerMessage;

function OnWorkerMessage(evt) {
alert("Worker's message: " + evt.data);
}

Within the JavaScript file of the worker thread (DedicatedWebWorker.js in our case) you would attach a function to the 'onmessage' event to receive messages from the thread that created the worker.

The following is an example of a worker thread attached to the onmessage event:

onmessage = OnCreatorMessage;

function OnCreatorMessage(evt) {
var sReturnMessage = ("Hello from the worker thread! This is what you sent my way: " + evt.data);

// Pass our message back to the creator's thread
postMessage(sReturnMessage);
}


Shared Web Workers

Shared web workers provide a way for any window, from the same origin/domain (SomeWebSite.com for example), to share the use of a worker thread.

Since worker threads take time to start initially and use a lot of memory per worker, being able to share the worker rather than creating one for each open window can improve performance. The trade off, however, is that the shared worker objects are a bit more involved to set up than dedicated workers are.

To create a shared web worker, you create an instance of the SharedWorker object and pass in a string indicating the location of the JavaScript file for the worker thread. The following is an example of creating a SharedWorker instance:

var aSharedWorker = new SharedWorker("SharedWorker.js");

To receive messages from the shared worker thread you attach to the 'onmessage' event but in a slightly different way compared to that of a dedicated web worker. In this case, you need to access the 'port' object of the shared worker instance and then attach to the port object's onmessage event as in the following example:

aSharedWorker.port.onmessage = OnWorkerMessage;

Rather than using the above method to attach to the onmessage event, you can also use the addEventListener function as in the following example:

// uses 'message' (not 'onmessage' as in previous examples) aSharedWorker.port.addEventListener("message", OnWorkerMessage, false);

// Required when using addEventListener
aSharedWorker.port.start();

The OnWorkerMessage function itself is shown below (no difference compared to how it worked with dedicated web workers):

function OnWorkerMessage(evt) {
alert("Worker's message: " + evt.data);
}

For each additional window that wants to connect with the shared worker, you simply repeat the above steps in each window.


Communication with the shared worker thread is achieved by means of postMessage calls on the port object. To pass a message to the shared worker thread you would do the following:

aSharedWorker.port.postMessage("Hello from Page 1");


Within the JavaScript file of the shared worker thread (in our case SharedWorker.js) you first attach to the 'onconnect' event which is called whenever a new object obtains a reference to the shared worker thread.

When the onconnect event fires you then attach to the onmessage event of the port passed in so that you can send/receive messages with the newly connected object.

// event triggered whenever an object attaches to this shared worker
onconnect = function(evt) {
// Get the port of the newly attached object. Attach to the onmessage
// event so that we can send/receive messages with the newly
// attached object

var port = evt.ports[0];
port.onmessage = function(e) { OnCallerMessage(e, port); }
}

function OnCallerMessage(evt, port) {
var sReturnMessage = ("Hello from the SharedWorker thread! This is what you sent my way: " + evt.data);

port.postMessage(sReturnMessage);
}


In Closing

I hope you found this post useful in understanding the basics of HTML 5 Web Workers.

For more detailed information the the HTML 5 Web Workers specification you can view it by clicking on the following link: http://dev.w3.org/html5/workers/


Update: If you are interested, I have now posted a follow-up to this article called 'A Deeper Look at HTML 5 Web Workers' which examines things like multiple shared workers, importing JavaScript files, and using the XMLHttpRequest object from within a worker thread.

New: I have recently written an article for DZone.com which combines the Web Worker information presented in my blog posts and introduces some new information like transferable objects, inline workers, and includes a link to a sample project stored on github.com. The article can be found here: http://refcardz.dzone.com/refcardz/html5-web-workers

14 comments:

  1. Why does the event object passed to onconnect contain an array of ports instead of just one?

    ReplyDelete
  2. onconnect seems like the place to initialize the state of the worker.
    Is there a way to determine that it is being called from a second page in order to skip re-initializing the state?

    ReplyDelete
  3. According to the specification for Shared Web Workers, an array of ports is always passed into the OnConnect event. At present there is only ever the one port passed in (array index 0). My guess is that this was done for future functionality.

    I haven't tested this but, with regards to the state of the worker being set during OnConnect, you could always have a global flag (var g_bInitialized = false;) where you would set it to 'true' when the OnConnect event is fired. When other pages connect, only initialize state if the flag is 'false'.

    ReplyDelete
  4. I don't think that would work because I think JavaScript global variables are tied to the Window. The two web pages would have separate Window objects and wouldn't share global variables. I guess I could try using HTML5 sessionStorage to keep track of this.

    ReplyDelete
  5. Usually global variables are tied to a Window but, in this case, the OnConnect event is fired from inside the shared worker which is in a separate thread from the window.

    The thread is shared by all windows that are connected

    ReplyDelete
  6. Excellent pieces. Keep posting such kind of information on your blog. I really impressed by your blog.
    html5 audio player| html5 audio player

    ReplyDelete
  7. Thanks for writing this up. Do you have a sense as to how web workers interact with local storage? Can they access it? Are there locks or other ways to handle race conditions? Thanks,

    ReplyDelete
  8. Hi Gerard, Here is a way to do cross-browser web workers (even in IE), so that they can be used in production applications:

    http://inficron.com/;mOEpX

    Hope you find it useful!

    ReplyDelete
  9. I have no words for this great post such a awe-some information i got gathered. Thanks to Author.
    html5 music player| html5 media player

    ReplyDelete
  10. Thanks for this post. I really got well basic information from it. Currently I am working for passing large data to worker and process on it and get again large result.Even I got successful to do it. But still something is going wrong taking it much time ..........

    ReplyDelete
  11. Great information shared nice article html5

    ReplyDelete