Web workers to the rescue! Web workers are supported by most moderns browsers with the exception of IE (Surprise!). IE10 release candidate supports them, but it is not very wide spread yet. More info can be found at Can I Use
How do Web Workers Work?
A web worker is just a plain Javascript file with anything in it. If you start
an empty file empty-worker.js
it will start up just fine and do absolutely nothing.
// empty-worker.js
To start a web worker you create a new Worker
and give the constructor a URL
as the only parameter. The URL must come from the same domain as the page
loading the Worker.
// This code goes inside a script tag or in a file loaded by <script src> // Start the empty worker, which does nothing. var worker = new Worker('empty-worker.js');
In order to have any use for our worker we need it to communicate with us. The
way a worker communicates is be sending messages. The method that does this
is called postMessage(object)
. It takes any type of argument, primitives as
well as arrays and objects.
// eager-worker.js postMessage('I am eager for work!'); // self.postMessage('I am eager for work!'); // Safest way
It is also possible to prefix the call with this
or self
, they both
refer to the same WorkerGlobalScope
. self
is the safest way since that
will not change with the calling context the way this
does.
Our eager-worker.js
starts up and posts a message and we need to receive it.
We can do that by setting the onmessage
property on our worker reference.
// In script tag or file loaded by script tag var worker = new Worker('empty-worker.js'); worker.onmessage = function(event) { console.log(event); };
Reloading the page will result in the following output in the console. Notice
that the data sent by the worker is available in the data
property of the
MessageEvent
MessageEvent {ports: Array[0], data: "I am eager for work!", source: null, lastEventId: "", origin: ""}
An alternative way of attaching a listener to the workers is to use
addEventListener('message', listener)
. Adding the event listener this way has
the advantage of allowing us to attach multiple listeners to the same worker. I
have not had the need for this yet.
worker.addEventListener('message', function(event) { console.log('One', event.data); }); worker.addEventListener('message', function(event) { console.log('Two', event.data); });
Reloading the page with the above code, will result in two lines in the console log. Notice that I am only logging the data part of the event.
One, I am eager for work! Two, I am eager for work!
Our eager-worker.js
is really eager to work so he keeps on telling his boss
that he want to work every second.
// eager-worker.js setInterval(function() { postMessage('I am eager for work!'); }, 1000);
This of course annoys the boss tremendously so he decides to tell the worker to
do something by sending him a message with postMessage
.
// main.js var worker = new Worker("eager-worker.js"); worker.onmessage = function(event) { console.log(event.data); }; worker.postMessage('Stop bugging me and do something!');
Our eager-worker.js
is not listening yet, so the boss can scream all he wants
without any success. Let's change that by implementing the onmessage
method
in the worker as well. addEventListener
also works.
// eager-worker.js postMessage('I am eager for work!'); var timer = setInterval(function() { postMessage('I am eager for work!'); }, 1000); onmessage = function(event) { clearInterval(timer); postMessage('Alright Boss!'); };
Now the output is less annoying.
I am eager for work! Alright Boss!
Now that we know the basics of web workers, lets look of some other interesting issues that come up.
Debugging Web Workers
If you try to use console.log
in your web workers you will get an error
messages such as this:
Uncaught ReferenceError: console is not defined
So this is an issue with web workers, it is not possible to use console
or
alert
to debug them.
It is not a big problem because with Chrome it is possible to debug workers. In
the lower right corner of the source tab of the Chrome Developer Tools, there
is a Workers panel
.
Checking the checkbox Pause on start
will open up a new inspector window
which allow us to debug the worker just as if it was a normally loaded script.
Nice!
Errors
If there are script errors in the web worker, it will send back an error
event
instead of a message
event. The errors can be handled via the onerror
property of by subscribing to the error
event.
worker.onerror(function(event) { console.log(event); }); // or worker.addEventListener('error', function(event) { console.log(event); });
The above code will result in an event that looks like this, showing you the filename and line number to handle the message.
ErrorEvent {lineno: 6, filename: "http://localhost/web-workers/eager-worker.js", message: "Uncaught ReferenceError: missing is not defined", clipboardData: undefined, cancelBubble: false…}
Web Worker Script Loading
A web worker can load additional scrips with importScripts(URL, ...)
. The
URLs can be relative and, if so, are relative to the file doing the importing.
importScripts('../data/swedish-word-list.js', 'word-maestro.js', 'messageHandler.js')
A larger example
In this example I will show how easy it is to create a delegating worker that allows me to call normal methods on an object.
The messages are sent using a simple protocol with an object containing two properties.
// The message object var message = { method: 'The method I wish to call', args: ['An array of arguments'] }
main.js
starts the delegating-worker.js
and sends messages to it.
// main.js var worker = new Worker("delegating-worker.js"); worker.onmessage = function(event) { console.log(event.data); }; setInterval(function() { // Call the method echo with the argument ['Work'] worker.postMessage({method: 'echo', args: ['Work']}); }, 4200); setInterval(function() { // Call the method ohce with the argument ['Work'] worker.postMessage({method: 'ohce', args: ['Work']}); }, 1100);
The delegating-worker.js
loads the external script echo.js
, which declares
the variable Echo
in the global worker scope. In the onmessage
method I
unpack the event and delegate the method call to Echo
via apply
. I use
apply since I want to allow a variable number of arguments. The reply is sent
back to main.js
along with the method that was called.
// Declares Echo importScripts('echo.js'); onmessage = function(event) { var method = event.data.method; var args = event.data.args; // I use apply since to allow a variable number of arguments var reply = Echo[method].apply(Echo, args); self.postMessage({method: method, reply: reply}); };
The Echo service is a simple object with two methods.
var Echo = { // Return the word recieved. echo: function(word) { return word; }, // Reverse the word and return it. ohce: function(word) { return word.split('').reverse().join(''); } };
Structuring the code in this way makes it easy to reuse the functionality in a non worker context.
Limitations of Web Workers
Since web workers are working in the background, they do not have access to the
DOM, window
, document
or even the console
. Any communication with these
objects will have to be done by sending messages back to the main script
Checking for Web Worker support
Checking for Web Worker support is easy, just check if window.Worker
is
defined and show an error page or use an alternative
solution if it is not.
function workersSupported() { return window.Worker; } if (!workersSupported()) { window.location = './unsupported-browser.html'; }
Wrap up
Using workers is easy, if you want to see a more thorough example, check out the source code (in Coffeescript) for Word-Maestro.
4 comments:
So without access to anything (DOM, window, etc) what are they good for?
I personally use them for heavy calculations such as permutations and searching through large wordlists.
See Word Maestro
Excellent tutorial on Web Workers. Is it possible to load jQuery into it for use of some of jQuery's non DOM methods?
I wouldn't recommend it, since jQuery uses both window and document at load time.
You are better off using something like Underscore
if you only want the utility methods.
(It is definitely possible to use jQuery, it is after all Javascript we are using :). But, you will have to create a lot of fake objects that simulate being window and document to get it to work. It's not worth it.
Post a Comment