There are lots of articles that explain and describe concurrency in JS from different angles. Ours will be practical enough to start using concurrency immediately. First, let’s look at the questions and answers about JS nature.
- Is concurrency possible in JS as a language itself? – No, it’s not. Java Script is single-threaded programming language with asynchronous operations support. (It can all live together because of the tricky queue/event management system.)
- Am I doomed if there’s a need for heavy operation handling in a JS application? – Again, no! There’s a great way to move the effort from the main thread away using something else…
… and this something else is our today’s hero – Web Worker!
In short, Web Worker is a modular script that runs separately from the main JS thread (a thread that normally includes all the scripts of your web application) and communicates with it using events only. There’re a few specific features that come from such a definition:
For better understanding, we’re going to create a small application with a very simple task: it’s going to calculate prime numbers until a given position, returning the feedback about the current state and progress to the main thread.
I’ve chosen this example because the prime number calculation algorithm is pretty heavy and can be a great tester for our worker, especially in its unoptimized form. We’re also going to show the list of requested states which will be fully scrollable and extendable with new statuses while Worker’s doing heavy calculations behind.
But let’s begin with the technical aspect and see how we can enable Web Worker:
let worker = new Worker('<your_worker_uri>.js');
One problem arises immediately. In current web development practices, all the scripts are normally minified and downloaded by the browser as a bundle (I mean popular frameworks, like Angular and React, which are commonly used with bundlers, like Webpack), while Web Worker constructor needs an absolute address of the script. Below we’re going to analyze a solution that’s suitable for Webpack + TypeScript (but can also be used with plain JS):
To include Web Worker sources into your bundle, you need to use worker loader in the following way:
import Worker from 'worker-loader!./<your_worker_uri>.js';
For TS integration, the process is a bit trickier:
- You need to specify module for worker loader definition (the easiest way is to specify [hash] variant of this module as a generic one, you can read about hash definition on the loader page, but, in simple words, it just generates hash-based name for loader during the bundling):
declare module 'worker-loader?name=dist/[hash].js!*' { const value: any; export = value; }
- Import worker using hash notation:
import Worker from 'worker-loader?name=dist/[hash].js!<your_worker_uri>'
Basically, that’s it. Worker can be written on JS or TS, and it doesn’t matter if the latter was included into webpack.config.js correctly. Don’t forget that you can and probably should import dependencies with such bundling through the import, so it’s not necessary to write all the logic inside one file.
After Web Worker is successfully attached, you can start with communication structuring. In the simplest case, communication will look like this:
Obviously, in both cases cases you should listen for the ‘message’ event using addEventListener and act accordingly like this:
self.addEventListener('message', message => { // Here goes code of your listener... }
By the way, in this example of code, we do listening for changes inside worker and variable self is a context of Worker where all the functionality lives. And here you can see another blurry specific of Workers: message event data is fully generic, so differentiation of such events fully lies on you. We wanted to support returning of the current progress state from Worker back to the main thread (at least to show valid progress bar and log statuses), which makes our chart a bit more complicated:
This scheme can be even more complicated if you want to process more than one task inside one worker or ping the main thread with a different kind of details. But the universal recipe is to use the following structure of the ‘message’ event data inside your application:
WorkerInput { type: string; // or enum preferably arg: any; }
Type property says what event we’re going to handle, while arg property contains all the required data for this kind of event. For example, in the upper case the code would be:
- For init worker event:
primeWorker.postMessage({ type: WorkerInputType.start, arg: { mode: WorkerMode.log, position: 100001 } });v
- For sending of current status back:
sendMessage({ type: WorkerResponseType.status, data: (this.active ? { active: true, position: this.currentPosition, prime: this.currentPrime, percentage: Math.floor(this.currentPosition / this.targetPosition * 100) } : { active: false }) });
The logic of handling for each individual type of event can be done with common switch and multiple handlers in the case of few simple operations (our case), but if you’re going to have a set of complicated algorithms behind event types in Worker, I would recommend you to use strategy pattern.
Now that we know how to send and receive data successfully, we can easily optimize the prime number calculation with Worker (any other heavy algorithm can be optimized in such way). The steps are:
- Move the whole algorithm that freezes the main thread inside the Worker;
- Add proper event handlers on both sides of communication (for the main thread those handlers will change DOM accordingly, e.g. move progress bar or add log into the list);
- If you plan to use Worker from the different parts of your app, control the number of instances (singleton class as a shell for Worker can be a good idea) not to initialize the same process twice;
I want to emphasize that Web Worker is not a silver bullet for any performance issue in your app. The reasons of page freezes can be very different, from implementing a memory leak to overloading your framework with heavy DOM operations. When you see freezes, it doesn’t mean, “It’s Web Worker time!” Check your code first, and only if you see that a specific part is causing a performance issue, you can try to move it aside via Web Worker. (But to be honest, you should know about such heavy calculations at the very beginning of your project, when you plan everything.)
The full code example can be found here. Thanks for your attention!
We are looking forward to meeting you on our website soshace.com