WebWorker — Creating an async task execution thread (updated)

Lokesh Pathrabe
4 min readJun 30, 2019

[Updated 21-Apr-2021] I have published a new web worker-util with a better way of executing functions on a worker thread. The repo link is given below. Please check it out.

Modern browsers are becoming more and more powerful day by day. With processor-savvy machines and mobile devices coming in, your browsers can handle heavy processing, graphics, and animation, you name it. But…… these capabilities come at a cost. While browser engines are busy processing heavy-duty loads, your interface may freeze. This results in a poor user experience. That’s where WebWorkers come into the picture.

WebWorkers have been around for a while now. They were designed to provide a non-blocking task execution thread. This thread can be used to compute processor expensive and time-consuming tasks without affecting the main browser thread. To know more about web workers and their different application you may refer to this article https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers

I was working on a small framework that would help to run tasks on web workers in an organized way. I had the following challenges to resolve:

1. A simple way to create and execute a task on a web worker thread
2. Communication between main browser thread and a web worker thread
3. Access to all the tasks running on the web worker

Now before we begin coding there are a few points to remember

  • The web worker thread is in complete isolation from the main thread. It can not access the window, document, and dom of the main thread.
  • It provides an API for communicating between the two threads, but only serializable data can be exchanged. This means we can not pass functions as callbacks.
  • The Web Worker provides an API postMessage() to exchange messages between the main and web worker thread.

Let's begin with a simple example of the addition of two numbers in web workers.

------ GlobalWorker.js --------
onmessage = function(e){
var num1 = e.data[0];
var num2 = e.data[1];

var result = num1 + num2;
console.log(result)
}
------ Main.js ------
Globals.worker = new Worker('path/GlobalWorker.js');
Globals.worker.postMessage([5, 6])

GlobalWorker.js is our js file which will be executed on the WebWorker thread.
Main.js will be executed in the browser main thread.

Challenge 1: Creating a global WebWorker object to add new tasks to the worker thread.

------GlobalWorker.js-------
Worker = {
init: function(){
onmessage = this.onMessage;
this.tasks = {}
},
addTask: function(task){
this.tasks[task.name] = task
},
onMessage: function(e){
var self = Worker
var name = e.data.name,
args = e.data.args
if(self.tasks[name] && self.tasks[name].main){
self.tasks[name].main(args)
}
}
}
function Task(conf){
var self = this
if(!('name' in conf && 'main' in conf)){
throw "Task must have a Name and Main method";
}
Object.keys(conf).forEach(function(k){
self[k] = conf[k];
})
return self;
}
--------Main.js--------
Globals.Worker = {
init: function(){
this.worker = new Worker('path/worker.js');
}
executeWorkerTask: function(taskName, taskArgs){
var args = {
name: taskName,
args: taskArgs
}
this.worker.postMessage(args)
}
}

Task constructors will create simple task objects. All tasks should have a name and main method property. ‘name’ property acts as an identifier and the ‘main’ method of Task will be executed when the task is registered from Main.js

Now that the basic structure is ready, let's create a simple task for the addition of 2 numbers

-----GlobalWorker.js------
var add_task = new Task({
name: 'add',
main: function(a, b){
console.log(a + b)
});
Worker.addTask(add_task)------Main.js------
Globals.Worker.executeWorkerTask('add', [5,6])

Challenge 2: Setting up communication between the 2 threads

Now since we want to be able to execute multiple tasks on web worker thread we will need a mechanism to allow tasks in GlobalWorker.js to communicate with Main.js. Let's add a map of listeners in Main.js. When postMessage() is called from web worker thread, these listeners will be executed. Here is the code

------GlobalWorker.js--------
Worker = {
init: function(){
onmessage = this.onMessage;
this.tasks = {}
},
addTask: function(task){
this.tasks[task.name] = task
},
onMessage: function(e){
var name = e.data.name,
args = e.data.args
if(this.tasks[name] && this.tasks[name].main){
this.tasks[name].main(args)
}
}
}
function Task(conf){
var self = this
if(!('name' in conf && 'main' in conf)){
throw "Task must have a Name and Main method";
}
Object.keys(conf).forEach(function(k){
self[k] = conf[k];
})
self.postMessage = function(data){
postMessage({
name: self.name,
data: data
})
}
return self;
}
var add_task = new Task({
name: 'add',
main: function(a, b){
this.postMessage(a+b)
});
})
Worker.addTask(add_task)--------Main.js--------
Globals.Worker = {
init: function(){
this.worker = new Worker('path/worker.js');
this.worker.onmessage = this.messageListener
this.listeners = {}
}
executeWorkerTask: function(taskName, taskArgs, taskListener){
var args = {
name: taskName,
args: taskArgs
}
this.worker.postMessage(args)
if(taskListener){
this.listeners[taskName] = taskListener
}
},
messageListener: function(e){
var name = e.data.name
var data = e.data.data
if(this.listeners[name]){
this.listeners[name](data)
}
}
}
Globals.Worker.executeWorkerTask('add', [5,6], function(result){
console.log(result)
})

Worker.addTask(add_task) that will add the Task we want to execute in GlobalWorker.js. Globals.Worker.executeWorkerTask(name, args, listenerFunc) will execute them from Main.js and attach a listener function. this.postMessage(a+b) will execute listener for task ‘add’ from the list this.listeners.

We can simply add new tasks and execute them as below.

------ GlobalWorker.js-------
var sub_task = new Task({
name: 'sub',
main: function(a, b){
this.postMessage(a-b)
});
})
var multiply_task = new Task({
name: 'multiply',
main: function(a, b){
this.postMessage(a*b)
});
})
Worker.addTask(sub_task)
Worker.addTask(multiply_task)
-------- Main.js --------
Globals.Worker.executeWorkerTask('add', [5,6], function(result){
console.log(result)
})
Globals.Worker.executeWorkerTask('sub', [5,6], function(result){
console.log(result)
})
Globals.Worker.executeWorkerTask('multiply', [5,6], function(result){
console.log(result)
})

Happy Coding !!

Lokesh Pathrabe

--

--

Lokesh Pathrabe

Web developer working mostly on ReactJS and other react eco system tools and technologies.