Jonathan Cardy, February 24th, 2011
In the previous post we set up a Web Worker helper function that allowed us to create a worker file, and call it using code like this:
$.work({file: 'primes.js', args: { from: 1, to: 100000 }}).then(function(data) { //Worker completed successfully console.log(data); });
Now, wouldn’t it be nice if you didn’t even have to write the worker file? To achieve this we must overcome the fact that Web Workers need to be constructed with a file name containing the Worker definition, as opposed to the function to be run. To get around this problem we just create a Web Worker that takes, as a message, a function definition and arguments, all encoded as a JSON-string.
This technique adds some code to our mission: to convert a function to and from a string requires a little bit of effort. To convert a function to a string we simply do this:
var funcStr = func.toString();
But the reverse – getting the function back from a string – is more difficult. We could try using eval:
var funcStr = func.toString(); eval("var func = " + funcStr);
However if you try that you will find that the performance of running the function in Chrome is abysmal: the function doesn’t get precompiled and the net result is that execution time is more than 10x slower. Another alternative is constructing the function using the new Function syntax. In the following table I compare the performance of each (all in milliseconds – lower is better):
| Native | Eval | new Function | |
|---|---|---|---|
| Chrome 9 | 207 | 2955 | 204 |
| IE 8 | 4078 | 4890 | 4047 |
| Opera 11 | 240 | 1080 | 240 |
| Firefox 3.6 | 341 | 342 | 336 |
In all cases, constructing the function using new Function gives the same performance as a natural JavaScript function, so we’ll use that instead of eval. In the following code we see how to convert a string-encoded function to a real function using the Function constructor. It just involves manipulating the function’s string to get the function body, and name of the function’s argument, and then pass those to the Function constructor.
//Get the name of the argument. We know there is a single argument //in the worker function, between the first '(' and the first ')'. var argName = funcStr.substring(funcStr.indexOf("(") + 1, funcStr.indexOf(")")); //Now get the function body - between the first '{' and the last '}'. funcStr = funcStr.substring(funcStr.indexOf("{") + 1, funcStr.lastIndexOf("}")); //Construct the new Function var newFunc = new Function(argName, funcStr);
And combining this knowledge into a “generic” Web Worker, we have the following code:
self.addEventListener('message', function (event) { //Get the action from the string-encoded arguments var action = self.getFunc(event.data.action); //Execute the newly-defined action and post result back to the callee self.postMessage(action(event.data.args)); }, false); //Gets a Function given an input function string. self.getFunc = function (funcStr) { //Get the name of the argument. We know there is a single argument //in the worker function, between the first '(' and the first ')'. var argName = funcStr.substring(funcStr.indexOf("(") + 1, funcStr.indexOf(")")); //Now get the function body - between the first '{' and the last '}'. funcStr = funcStr.substring(funcStr.indexOf("{") + 1, funcStr.lastIndexOf("}")); //Construct the new Function return new Function(argName, funcStr); };
Note that in the above worker we attach to the message event using the standard addEventListener syntax. That is much nicer than the old school method of adding a function to the onmessage property, and allows us to attach multiple listeners if needed.
To consume this Web Worker we must serialise the a function to be run, and its arguments, so they can be passed in a message. Our $.work function can do that for us. We’ll also add one other detail: make it cross-browser compatible by synchronously executing the action when there is no Worker definition.
$.work = function(action, args) { var def = $.Deferred(function(dfd) { if (window.Worker) { var worker = new Worker('worker.js'); worker.addEventListener('message', function(event) { //Resolve the Deferred when the Web Worker completes def.resolve(event.data); }, false); worker.addEventListener('error', function(event) { //Reject the Deferred if the Web Worker has an error def.reject(item); }, false); //Start the worker worker.postMessage({ action: action.toString(), args: args }); } else { //If the browser doesn't support workers then execute synchronously. //This is done in a setTimeout to give the browser a chance to execute //other stuff before starting the hard work. setTimeout(function(){ try { var result = action(args); dfd.resolve(result); } catch(e) { dfd.reject(e); } }, 0); } }); //Return the promise to do this work at some point return def.promise(); };
To define the code, you can write any function that takes a single parameter:
//Define a function to be run in the worker. //Note that this function will not be run in the window context, and therefore cannot see any global vars! //Anything this function uses must be passed to it through its args object. var findPrimes = function (args) { var divisor, isPrime, result = [], current = args.from; while (current < args.to) { divisor = parseInt(current / 2, 10); isPrime = true; while (divisor > 1) { if (current % divisor === 0) { isPrime = false; divisor = 0; } else { divisor -= 1; } } if (isPrime) { result.push(current); } current += 1; } return result; }
And running it then becomes this succinct beauty:
$.work({action: findPrimes, args: { from:2, to:50000 }}).then(function(data) { alert('all done'); }).fail(function(data){ alert('oops'); });
Performance
To try out the performance I’m going to do the following 3 tests in the usual 4 browsers:
- Run the findPrimes function in the UI thread (no workers involved)
- Run the findPrimes function in a Web Worker (keeping the UI thread free)
- Run the findPrimes function in two Web Workers (splitting the calculation into two equal parts)
| UI thread | One worker | Two workers | Observations | |
|---|---|---|---|---|
| Chrome 9 | 4915 | 4992 | 3268 | CPU at 50% with 1 worker, 100% with 2 |
| Firefox 3.6 | 7868 | 7862 | 5289 | CPU at 50% with 1 worker, 100% with 2 |
| Opera 11 | 5754 | 5780 | 5676 | CPU at 50% in both cases (but UI thread is free) |
| IE 8 | 108689 | same | same | CPU at 50% in all cases (UI thread is always used) |
In the above tests we can see that execution always takes the same time in a Web Worker as in the UI thread. In Chrome and Firefox, we see that executing Web Workers concurrently gives a nice performance improvement by taking advantage of multiple CPUs on the user’s machine. These are very positive results, especially considering the overhead in constructing and messaging the Web Workers.
Chrome
This technique of wrapping Web Workers works really well in Google Chrome, even though, as we saw in Part 1, Chrome has the largest overhead in constructing a Web Worker object. As you would expect, Chrome makes use of multiple cores by running the Web Workers in separate threads and we can achieve a good speed-up in performance on multi-core machines vs single-core machines.
Firefox
Firefox also has great performance. There is a good speed-up on multi-core machines, and additionally, Firefox has a low overhead in constructing Web Worker objects.
Opera
Although Opera does support Web Workers, it doesn’t seem to run them in their own threads – in the table above we can see that the performance when running multiple workers is no better than running a single worker, when on a multi-core machine. I noted that the CPU usage maxed out at 50% on my dual-core machine even though I was running multiple workers. I’m sure Opera will resolve this in the future though, and using Web Workers still frees up the UI thread and makes the browser responsive during long-running calculations.
Internet Explorer
In IE, since we are executing exclusively in the UI thread, long-running calculations will result in the message “A script on this page is causing Internet Explorer to run slowly”. This will occur if your worker function executes more than 5 million statements. By way of workaround I can only suggest the following:
- Split the worker into multiple smaller workers to ensure the 5 million-statement limit is not reached. You should endeavour to provide user feedback regularly to let the user know that the application has not crashed.
- If you have control over the client’s registry (eg. a company internal application) then the limit can be changed, although that is a bad idea because the browser will be unresponsive for a long time.
- Offer an alternative version of your application to IE users, which is not as computationally intensive. Inform the users that they can use the full version in another browser.
This entry was posted on Thursday, February 24th, 2011 and is filed under Blog.
You can follow any responses to this entry through the RSS 2.0 feed. You can leave a response, or trackback from your own site .

I think you could use an id system to avoid loading the Worker’s file multiple times:
- load the worker once,
- whenever $.work is called:
+ create a new id
+ create a deferred and store it in a cache ( deferreds[ id ] = defer )
+ send the id to the worker together with the action and the args
+ when the worker is finished, have it send the id together with the result
+ then, client-side:
– retrieve the deferred linked to the id ( deferreds[ i ] )
– remove it from the cache
– resolve it
I may be off-base since I dunno web workers that well, but don’t you think it could be possible?
Hi Julian!
I think you are suggesting to load a single Worker, and send work to it each time $.work is called? If so, then calling $.work twice would result in the first being executed, and the second queued. I actually did implement your idea while writing this article but found that it was necessary to load a new Worker for each concurrent work item. However, it might be possible to spawn child Workers within the main Worker, I haven’t investigated that.
Anyway, I think that when the browser re-requests the JavaScript worker file it should get a 304 not-modified response and not have to download it again.
Thanks for your comment!
Johnny
Great stuff! thanks for sharing. I was looking for a general purpose implementation of web workers. And I also learned about jQuery deferred objects, which is pretty cool as well.