Introduction to Web Workers
Web Workers are created by writing a block of code in a separate js file. This piece of code is then executed in an entirely separate context - it has no access to the window object, it can't see the DOM, and it receives input and sends output via messaging. The messages are serialized, so the input and output is always copied - meaning we can't pass any object references into workers. Although initially this seems like a serious downside, it can also be viewed as a great bonus - it forces thread safety.
To implement a worker, we have to create worker code in a new file. It needs to confirm to a specific "interface":
implement this function to receive messages from UI thread
implement this function in a Shared Worker, to receive notification when multiple UI threads (ie. from multiple windows) connect to the same worker instance
call this function to send a message back to the UI thread
Since a Worker doesn't have access to the window object, you can't use all the window functions you are used to (self is the global object in a Web Worker). However you can still use these:
Here is a simple implementation of a prime-number calculating Worker, primes.js:
Here we have implemented a function called onmessage, and that function calls postMessage with its results. To make use of this Worker, we have the following piece of code in our page:
This constructs a new Worker object using our worker definition file. Each time it receives a message from the Worker, it is output to the console.
But this post isn't supposed to be a Worker tutorial: on to the performance measurements.
Worker thread vs. UI thread
To run the following tests, I updated the worker above by adding timestamp measurements at the start and end of the onmessage function. They are then passed out through the result object at the end. This allowed me to get the exact time when the function started and finished execution, enabling measurement of the time taken to send a message to the worker, the time for it to execute, and the time for it to send a message back to the UI thread.
I also ran the same algorithm without the use of any workers. The parameters in both cases were from 1 to 100000. Everything was repeated in Chrome, Firefox, Opera and IE.
In Chrome, the Worker execution time is a little longer than the UI thread, and the setup time is bigger than the other browsers. Since this is a constant, it will become less significant as the Worker does more work, or is reused.
In Opera, execution also takes a little longer in the worker, but again the setup time is a bigger factor as with Chrome.
In Firefox, the Worker is more than twice as fast! I don't know why this is. My only guess is that the UI thread is busy doing other things. The setup time is minimal. Firefox seems to like workers, but in saying that, it's still slower than Chrome and Opera.
Multiple Workers vs. Single Worker
In all of the tests above, core 1 of my dual-core CPU shot to 100% usage while core 2 remained idle. That's a bit of a waste, and that's where the benefits of Web Workers should be seen.
So let's repeat the tests above, using two workers instead of one. IE is left out this time for obvious reasons. All timing is in milliseconds.
(load to completion)
Consistently we see that two workers are only slightly faster than one, but that is entirely due to the overhead involved in creating each worker - the actual execution time doubled in speed.
But there is definitely something strange going on with Opera! The time taken to construct the workers is almost equal to the total time required. This means the UI thread is busy whilst the workers are running, and the UI thread won't get to see any benefits as is the case with Chrome and Firefox. If Opera was more popular I'd spend time investigating this quirk, but it's not, so I won't!
Sending/Receiving Large Messages
Workers communicate with the UI thread via messaging, and those messages are copied. If we pass an object to a Worker, it's serialised to JSON, and this serialisation and copying process is going to require effort. Let's measure exactly how much effort. I've removed the work from the Worker and I simply pass it an object, and it pings that object back. We take a timestamp within the Worker so we know exactly when it's run. This is the Worker code:
And this is how we consume it:
For each browser, I ran the above code both with and without a large (100KB) object in the postMessage argument. This let me find the time delta which indicates the time lag induced by passing the object. Again all times are in milliseconds.
|Browser||Send empty||Receive empty||Send large||Receive large||Send large
I think we can safely conclude that serialisation/deserialisation and message passing doesn't take a significant amount of time, especially compared to the overhead of constructing the worker.
Sometimes workers take longer to execute than they would in the UI thread, sometimes they're faster. However, doing work in a Worker means the UI thread is free to concentrate on a responsive UI. Of course, any potential benefits are totally lost if the Worker is only doing a little work, because it's not worth the overhead required to construct the Worker.