Efficient communication for web workers
Efficient communication for web workersβ
Hello readers, welcome to my new article on web worker communications. I hope you enjoyed my previous article on multi threading on the web. In this article, we will discuss how to scale web workers and handle worker responses more efficiently.
[ Quick refresher ]
**Creating Web worker - ** >
const worker = new Worker("./worker/filepath");
>
**Sending data to web worker - ** >worker.postMessage(data, buffer?)
;
**Getting data from web worker - ** >worker.onmessage = (data) => { /* logic*/ }
Scaling problemβ
In the previous article, we discussed how to offload a heavy thread-blocking task to web workers. But in real production environments, we might have multiple such tasks that we need to offload. In this segment, we will discuss how to handle them gracefully.
Let's consider we have 2 heavy functions:
- Find the Fibonacci series for the first 100000000000 numbers
- Sum of first 100000000000 numbers
So, what should be our approach π€ ?? . . .
An Okay-ish Solutionβ
After thinking for an eternity, I decided to exploit the following principles in a genius way
- KISS (Keep It Simple Stupid) Principle
- Sepaarations of concerns principle.
Let's create two web workers for both functions and problem solved π.
Awesome !!! But now we got another requirement, where we need to add 3 more heavy functions (5 in total).
My lazy brain immediately said: No problem create 3 more web workersπ.
But deep inside, I knew how dumb this idea is. The whole point to use web worker is to improve performance. Web workers internally use actual threads on OS. If we open these many workers at the same time, it will use the CPU heavily and have a negative impact on performance.
Ok, Now let's think again... But this time with a single web worker.
A Better Solutionβ
Have you got any lead on how to proceed? If Yes, please give it try before proceeding to the solution part. In case you have not figured out any approach, let's do it together.
What if we can define all the functions in a single web worker and based on incoming data, the web worker will figure out which method to invoke and call them with proper inputs? Confused ?? Let's implement it have some clarity.
// Worker.js
const works = {
heavyWorkOne: (param) => {
/* some heavy work */
},
heavyWorkTwo: (param) => {
/* some heavy work */
},
heavyWorkThree: (param) => {
/* some heavy work */
},
heavyWorkFour: (param) => {
/* some heavy work */
},
heavyWorkFive: (param) => {
/* some heavy work */
},
};
self.onmessage = (data) => {
const { methodName, input } = data;
const args = Array.isArray(input) ? input : Array.of(input);
const result = works[methodName].apply(this, args);
self.postMessage(result);
};
The clean and clear solution, right? But there it one problem here. As the methods increase, file size also increases. Which will make it hard to maintain the file.
Can we create separate files for each method and import them into the worker files? Can we use ES import in workers?
The answer is Yes β
const worker = new Worker('./path/to/worker', { type: 'module' });
If we specify, type as module
, while creating web worker, then it will allow us to use es imports.
// Worker.js
import heavyWorkOne from './heavyWorkOne';
import heavyWorkTwo from './heavyWorkTwo';
import heavyWorkThree from './heavyWorkThree';
import heavyWorkFour from './heavyWorkFour';
import heavyWorkFive from './heavyWorkFive';
const works = {
heavyWorkOne,
heavyWorkTwo,
heavyWorkThree,
heavyWorkFour,
heavyWorkFive,
};
self.onmessage = (data) => {
const { methodName, input } = data;
const args = Array.isArray(input) ? input : Array.of(input);
const result = works[methodName].apply(this, args);
self.postMessage(result);
};
Now it is more scalable and easier to add new functionalities to it.
Communication Problemβ
Excellent, now that our worker is ready, let's give it a try.
const worker = new Worker('./path/to/worker', { type: 'module' });
const inputEl = document.getElementById('dummyInput');
inputEl.addEventListener('click', () => {
worker.postMessage({ methodName: 'heavyWorkOne', data: 100000000 });
worker.onmessage((result) => {
console.log(result);
});
});
inputEl.addEventListener('change', () => {
worker.postMessage({ methodName: 'heavyWorkOne', data: 100000 });
worker.onmessage((result) => {
console.log(result);
});
});
inputEl.addEventListener('keyup', () => {
worker.postMessage({ methodName: 'heavyWorkFour', data: 100000000 });
worker.onmessage((result) => {
console.log(result);
});
});
inputEl.addEventListener('blur', () => {
worker.postMessage({ methodName: 'heavyWorkFive', data: 100000000 });
worker.onmessage((result) => {
console.log(result);
});
});
And here we go.. another bunch of problems.
- Code duplication
- multiple listeners
- All four onmessage will be notified for the results others message. We can not figure out which result came for which message. (Major problem)

Communication Layer between worker and main jsβ
To Solve the above issues, we will create an intermediate layer. This layer will
- create web worker
- handle onmessage event
- return appropriate results to the respected message sender
class CommunicationWorker {
constructor(workerFilePath, options) {
this.worker = new Worker(workerFilePath, options);
this.worker.onmessage = this.messageHandler.bind(this);
this.activeCalls = new Map();
}
send(data) {
const id = Math.round(Math.random() * 100000);
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
this.activeCalls.set(id, [resolve, reject]);
this.worker.postMessage({
id,
...data,
});
return promise;
}
messageHandler(e) {
const { error, id } = e.data;
const [resolve, reject] = this.activeCalls.get(id);
if (error) {
reject(e.data);
} else {
resolve(e.data);
}
this.activeCalls.delete(id);
}
}
Let's update our previous code using this layer.
const worker = new CommunicationWorker('./path/to/worker', { type: 'module' });
const inputEl = document.getElementById('dummyInput');
inputEl.addEventListener('click', async () => {
const result = await worker.send({
methodName: 'heavyWorkOne',
data: 100000000,
});
});
inputEl.addEventListener('change', async () => {
const result = await worker.send({
methodName: 'heavyWorkOne',
data: 100000,
});
});
inputEl.addEventListener('keyup', async () => {
const result = await worker.send({
methodName: 'heavyWorkFour',
data: 100000000,
});
});
inputEl.addEventListener('blur', async () => {
const result = await worker.send({
methodName: 'heavyWorkFive',
data: 100000000,
});
});
Voila !! We not only fixed all the above problems but also promisified the process. Now the code is much more readable and maintainable.
I hope, it will help you in your development journey, If you have any better ideas, please share them in the comments. It will be great learning for me and our community.
Thank you so much for reading my article. If you find it useful, please give your reactions. I will be happy to receive feedback.
Have a great day ahead.