Why Web Workers Make JavaScript More Than a Single Thread

July 23, 2018 0 Comments

Why Web Workers Make JavaScript More Than a Single Thread

 

 

It’s been drilled into our heads that JavaScript is not concurrent, that it’s just a single threaded language. Though that may be technically true, the problem is that, unlike other scripting languages, JavaScript was created to interact with web browsers. So, if you want to do anything computationally demanding, you do that at the cost of lots of jank, thanks to single threading.

To those uninitiated to the ways of jank, this phenomenon is caused by code that blocks the runtime from rendering important content. It’s the user having to twiddle their thumbs at a useless interface. That’s not good UX(User Experience).

Then, how do websites function nowadays? Is there some Jedi magic going on that allows JavaScript developers to excuse themselves from ever running any code that computes anything at all? Well, the answer might be yes actually. You can bind C++ code to JavaScript with the help of libraries, but this is used within the Node environment. That’s the environment in which JavaScript is more synonymous with Ruby and all those other back end scripting languages.

We’re talking about client-side JavaScript here, and, no, there is no magic in this realm, though some may beg to differ.

Workers allow you to run background threads so that your main program can take care of simple logic. This is where the underworld of Web APIs meets JavaScript’s runtime, and this is why JavaScript is technically single threaded.

See, JavaScript knows it’s supposed to run one piece of code at a time, but it has no problem making shady deals under the table. These shady deals come in the form of asynchronous calls. You know, the usual setTimeout(). That’s a web API and not part of JavaScript’s core language.

A Worker is also an API that we can utilize to implement concurrency. If you have doubts, this is nothing new. It has nothing to do with es6. It’s been around the block — so much so that it’s actually widely supported. Of course, IE 10 support is as good as you’re going to get.

How does it work? It’s actually easy to understand. Here’s the work flow of a program with a worker:

main.js:main thread      | worker.js: worker thread
1.new worker created     |
2.post a message ->      | 3.catch the message => {do something…}
5. catch the result      | 4. <- post the result
  1. In our main.js file, we instantiate a instance of the Worker object like so:
main.js: let worker = new Worker('worker.js');

Note that main.js can be any JavaScript file containing the data that you want to pass on to the worker to undergo computation.

2. We then post a message.

let myObj = {name: 'Ozymandias', age:200 };
main.js: postMessage(myObj);

3. On the worker.js side of life, we add an event listener to self. You can say that your worker js file is wrapped by a Worker class. Any event listener you add is a reference to itself.

worker.js:
addEventListener('message', (e)=>{
e.data // {name: 'Ozymandias', age:200}
});

4. After making changes to our data, we can send the results back with another postMessage()`

worker.js:
addEventListener('message', (e)=>{
e.data; // {name: 'Ozymandias', age:200}
postMessage(e.data);
});

5. Now we can catch it in our main.js.

main.js:
worker.addEventListener('message', (e)=>{
e.data // {name: 'Ozymandias', age:200}
});

Now that we have an idea of how Web Workers work, let’s actually use a semi-wild example to illustrate their overwhelming benefit. Here’s the scenario. Geo Pixer, a photophile, wants to build an image sharing site called GPix. He’s written the JavaScript code below to prototype the uploading proccess.

When he uploads a 2400x2000 image, this is how the code performs:

If you look at that red bar, that’s Chrome warning Geo that his code is full of jank.

Sure, one culprit of the jank is the opacity function he created. But the two load() event listeners(the ugly yellow block) contribute a major portion to the jankification of Geo’s code. That’s because the image has to be decoded before it can load.

So, Geo stares at a blank screen for three seconds before the image shows up. That’s a no, no.

Here’s a refactored version of the code.

Note:createImageBitmap() is an alternative to the fileReader() API. You can see immediately that createImageBitmap() is promise-based, which means there are background processes that decode the image. Then.thenwaits for the decoding to complete to start drawing the image.

When Geo goes to test the code in the browser, he gets this:

No red bar! Those dashed lines indicate that there’s a seperate thread in the background.

Lo and behold:

Chrome actually schedules a worker to decode images off of the UI thread if you use createImageBitmap().

This isn’t to evangelize createImageBitmap(), because it’s relatively new and is unsurprisingly not supported by Internet Explorer. What you can take away from this is how fast you can feed information to your users if you offload taxing functions to a worker.

Workers are JavaScript’s dirty little secret(they’re not really a secret), so take advantage of them.

You probably noticed that the opacitor function contained a nested for loop. That’s a O(n²) level of trouble. Every pixel is traversed. With an image that’s 2400x2000, that’s a boat load of pixels to have to analyze.

Challenge: Why don’t you try to refactor that code even further by using a worker?

You shouldn’t equate the flow of data from one file to another to the way a ball is passed from one athlete to another. The better analogy is the way a Fax machine works. Your document isn’t sucked up by the machine and zipped to the recipient. Imagine how long it would take for them to receive that document. This data isn’t shared, it’s copied.

Yes, this is called a shared worker. The implementation differs from our dedicated worker example. It’s not as widely accepted, but if you want to learn how to use them read Mozilla’s great guide.


Tag cloud