How to pass function to Web Workers

Web Workers allow for something similar to multithread operations that are common in languages such as Java or C++. In front end world, they are tool which is yet to be more frequently used. That’s, in my opinion, mainly due to lack of knowledge about their use cases, misjudgment that most people have fast PCs and modern browsers, and force of habit of sticking to what one knows (I second that). Additionally, there some limitations to their usage, such as the inability to pass functions into them.

Why to use Web Workers

Web worker is a mere script that runs in the background, in another thread, which means that any calculation, however expensive, will not block the UI’s thread. That’s huge. People hate when websites become slow or even worse, non-responsive. With Web Workers, you can do the heavy lifting in the background while showing the user a loading indicator and letting him or her do whatever else in the meantime.

You may ask when this is useful. We’ve put this into good use when we worked with a recursive tree structure. We were processing the whole tree with thousands of nodes each time the user interacted with the tree. That included loads of calculations and had we had done all of that in the main thread + render the result at the end, even the most beastly pc sets would have growled about it.

Limitations of Web Workers

Since the Web Workers run in another thread, there are limitations to what it can and cannot do.

  • It cannot access the DOM directly and you lose direct access to window object.
  • you cannot rely on a global state within them.
  • you cannot send in data that cannot be handled by structured clone algorithm

The last has turned out to be the most painful for me. You see, when you have an instance of a Web Worker, you may send in data through postMessage

worker.postMessage({
    string: 'string',
    number: 0,
    array: [],
    ...
});

Those value types above can be handled by the structured cloning. However, you cannot send in functions because they can be neither cloned nor transferred. That was a problem, because we wanted to send in an evaluator for each of the nodes (e.g. whether its name matches a search term), for which we needed a function inside of the worker.

Overcoming the no-functions limitation

There is a simple trick on how to solve this. As any other object or value, and in javascript especially since functions here are First-class citizens, we may define functions within objects and stringify them through JSON.stringify. This transforms the function declaration a bit, so parsing them back requires a bit of effort. Luckily, there’s JSONfn plugin that handles it well both ways.

And that’s that is required. Now you can declare and object which includes functions for the Web Worker, stringified with JSONfn:

// From main thread
worker.postMessage({
    myFunction: JSONfn.stringify( (arg) => ... )
    payload: ... // any kind of data, let the function decide whether it's useful
});

And reconstructed inside of the Web Worker’s script

// inside of worker
self.addEventListener("message", function(e) {
    // `e.data` contains data sent from main thread
    const myFunction = JSONfn.parse(e.data.myFunction);
    myFunction(e.data.payload); // reconstructed and callable
});

As you can see, if we have several functions which need different arguments, we have to be careful. This is similar to an observer pattern in the way commonly implemented in C# for instance, where several observers subscribe to the dispatcher, and when they receive data, the observers have to handle the payload and decide whether they can use that message. Here it is the same. The most scalable option I’ve found is to have two stable parameters, one for functions and one for their arguments a simply send it both in arrays. Then when the event occurs, send all the arguments to all the functions and let them handle it.

A side note about classes

Be aware that the above-described approach will not work if you use classes. Although from the maintainability and readability perspective it would have made a perfect sense to use classes and typescript interfaces, it is not possible. The stringify method can turn to a string only those values it has access to directly. However, when you define something as a class method, it is attached merely to the object’s prototype. It does not directly exist on the object itself.