Web Workers For Beginners
By: Kirti Chaturvedi
Tags: Programming, JavaScript, React, Webworker, Web Development

JavaScript is a single-threaded language meaning that it executes code in order and must finish executing a piece of code before moving onto the next.

For HTTP requests, JavaScript provides us with XMLHttpRequest which is non-blocking.But if we have a CPU intensive code in the program, it will block the main thread until it’s done, making UI unresponsive for that period. One way of solving it is to use setTimeout or setInterval. We can break the CPU intensive code into smaller parts and attach timeout with it. While it may work for smaller programs, as code complexity increases it becomes difficult to decide where to add timeout functions. What we need is a separate thread where these calculations are done, hence making the main thread free to handle UI events, manipulate DOM, or do other tasks.

Web Workers to the rescue

HTML5 introduced Web Worker to us. Web Workers are JavaScript programs running on a different thread, in parallel to the main thread. It allows you to put long-running and computationally intensive tasks on the background without blocking the UI, making your app more responsive. Worker utilizes a thread-like message passing between the main script and the background script to achieve parallelism.

Types of Web Workers

By creating separate threads for parallel JavaScript program Worker thread allows us to achieve multithreading. There are two types of Web Workers.

⦁ Dedicated Workers: Dedicated Web Workers are instantiated by the main process and can only communicate with it.

⦁ Shared Workers: Shared workers can be reached by all processes running on the same origin (different windows, browser tabs, iframes, or other shared workers).

This article will only cover dedicated workers and I’ll refer to them as ‘web workers’ or ‘workers’ throughout.

Getting started

Web Workers run in an isolated thread in the browser. Hence the code they execute is contained in a separate JavaScript file and incorporated in your page by asynchronous HTTP requests. For creating a worker thread we add the following lines on our main page.

var worker = new Worker(‘calculate.js’);

If the “calculate.js” file exists and is accessible, the browser will spawn a new thread that downloads the file asynchronously. Right after the download is completed, it will be executed and the worker will begin. In case the provided path to the file returns a 404, the worker will fail silently.

To start the worker, we have to call a postMessage() method.

worker.postMessage(message, [transfer]);

Here the message is the object to deliver to the worker page. A transfer is an optional array of Transferable objects (we will talk about Transferable objects in a later section).

Communication between the web worker and the main page occurs by two methods the postMessage() method and the “message” event handler.

postMessage can accept either a string or JSON object as its single argument. Newer browsers support a JSON object as a first parameter to the method while older browsers support just a string. When a message is sent from postMessage() the other page will receive the event as a ‘message’ event. For error handling, an ‘error’ event is passed.

In the example above, main.html calls the two web workers (prime and factorial)with postMessage. I have used separate web workers for prime and fibonacci methods, both methods symbolize large computations.

primeWorker.postMessage({cmd:'first50'});



fibonacciWorker.postMessage({cmd:'first100'});

The worker files are themselves listening to the ‘message’ event and as soon as they receive an event they do their calculations and send the result back with the help of self.postMessage method. I have also added a third button to change the background color, it symbolizes UI changes. When you call prime or factorial from the script an independent parallel web worker thread is spawned which will work on these methods and parallelly you could make changes in UI by clicking the change background button.

In the main.html file, I have added an error event listener also.

fibonacciWorker.addEventListener('error', errorEvent, false);

if an error occurs while a worker is executing, the ErrorEvent is fired. This interface contains three useful properties for figuring out what went wrong: filename — the name of the worker script that caused the error, lineno — the line number where the error occurred, and message — a meaningful description of the error.

You must have noticed I have used “self” in prime.js and “this” in fibonacci.js, its because in the context of a worker, both “self” and “this” reference the global scope for the worker.

Here if we haven’t used web workers then the browser would have frozen for the time the fibonacci and prime functions were computing and so you wouldn't be able to interact with UI at that time.

Importing scripts and libraries

Worker threads can import an external script using the importScripts() method. It accepts zero or more URIs as parameters to resources to import. The browser loads each listed script and executes it. Any global objects from each script may then be used by the worker. If the script can’t be loaded, NETWORK_ERROR is thrown, and the subsequent code will not be executed.

importScripts(‘foo.js’); /* imports just “foo.js” */ importScripts(‘foo.js’, ‘bar.js’); importScripts('//example.com/hello.js');

Subworkers

Workers may spawn more subworker thread. This is great for further breaking up large tasks at runtime. However, subworkers have some conditions of their own:

⦁ Subworkers must be hosted within the same origin as the parent page.

⦁ URIs within subworkers are resolved relative to their parent worker’s location. This makes it easier for workers to keep track of where their dependencies are.

But while spawning subworkers we have to be cautious about not hogging too many of the user’s system resources.

Embedded Workers

If you want to create a worker script on the fly or don’t want to create separate files for workers. You can embed it all in a single page with the help of Blob and using Data blocks.

With Blob(), you can “inline” your worker in the same HTML file as your main logic by creating a URL handle to the worker code as a string.

let blob = new Blob([

"onmessage = function(e) { postMessage('msg from worker'); }"]);



// Obtain a blob URL reference to our worker 'file'.

let blobURL = window.URL.createObjectURL(blob);

The magic happens in window.URL.createObjectURL().This method creates a simple URL string that can be used to reference data stored in a DOM File or Blob object. Blob URLs are unique and last for the lifetime of your application(e.g. until the document is unloaded).

Now for the clever part. The script element that does not have an src attribute or has a type attribute that does not identify an executable MIME type won’t be parsed by JavaScript(Data block). So we can write our worker code in this script tag and then use Blob to create a URL for the same.

In the example above, in script id “worker-1” I have added the code for web worker and then in JavaScript parsed script created a Blob URL for the same.

Transferring data to and from Workers

When we use postMessage we transfer data between web workers and the main page. This data is copied not shared between two pages i.e Objects are serialized as they’re handed to the worker, and subsequently, de-serialized on the other end. Most browsers implement this feature as structured cloning. This seems okay for small data transfer but for large amounts of data, this is a noticeable overhead.

To mitigate this performance hit we can use a Transferrable object. With Transferable Objects, data is transferred from one context to another. It vastly improves the performance of sending data to a Worker. Think of it as pass-by-reference if you’re from the C/C++ world. However, unlike pass-by-reference, the ‘version’ from the calling context is no longer available once transferred to the new context. For example, when transferring an ArrayBuffer from your main app to Worker, the original ArrayBuffer is cleared and no longer usable. Its contents are transferred to the Worker context.

To use transferrable objects the following command is used

worker.postMessage(arrayBuffer, [arrayBuffer]);

The first argument is the data and the second is the list of items that should be transferred.

Features available to Web Workers

Web Workers have access only to a subset of JavaScript features due to their multi-threaded nature. Here’s the list of features:

⦁ The navigator object

⦁ The location object (read-only)

⦁ XMLHttpRequest

⦁ setTimeout()/clearTimeout() and setInterval()/clearInterval()

⦁ The Application Cache

⦁ Importing external scripts using importScripts()

⦁ Creating other web workers

⦁ Web Worker limitations

Web Workers don’t have access to some very crucial JavaScript features:

⦁ The DOM (it’s not thread-safe)

⦁ The window object

⦁ The document object

⦁ The parent object

Use cases

Web Workers have great powers, but like the saying goes “With great power comes great responsibility”. Similarly, we should be cautious while using them. It should be preferred where background tasks have to be done in parallel to the main UI task or the tasks performed have to be hidden from the user. Some of its other use cases could be:

⦁ Prefetching and/or caching data for later use.

⦁ Code syntax highlighting or other real-time text formatting.

⦁ Spell checker

⦁ Progressive Web Apps

⦁ Analyzing video or audio data.

⦁ Background I/O or polling of web services.

⦁ Processing large arrays or humungous JSON responses.

⦁ Image filtering in <canvas>.

⦁ Updating many rows of a local web database.

Please Note: Due to security issues you can’t run your web workers locally on the browser. I used Web Server for Chrome, which worked great.

Thanks for taking out the time to read the article. In the next article, I will talk about Service Workers, a different but very useful web worker.