How to run a JavaScript pure-function on a separate thread via Web Workers
Web workers are essential in web development as they allow developers to run computationally heavy or time-consuming tasks in the background, separate from the main JavaScript thread.
By offloading these tasks to web workers, the main thread remains responsive, ensuring a smooth user experience.
Web workers enable parallel execution, improving performance by leveraging the power of concurrent processing. This capability enhances the overall responsiveness and efficiency of web applications, making them capable of handling complex calculations and resource-intensive operations without impacting the user interface.
In our example, we will try to create a generic (all-purpose) worker which will execute whatever function we pass into it with given payload.
1) Base Example
index.html
<!DOCTYPE html>
<html>
<head>
<title>Web Worker Example</title>
</head>
<body>
<h1>Web Worker Example</h1>
<button onclick="runWorker()">Run Web Worker</button>
<script>
// Create a new web worker
const worker = new Worker('worker.js');
// Send payload and function to the web worker
const payload = { message: 'Hello, worker!' };
const functionToExecute = (data) => {
console.log(`Received payload in the worker: ${data.message}`);
};
worker.postMessage({ payload, functionToExecute });
</script>
</body>
</html>
We only have a button on this page which will execute the function runWorker (which we will create) upon a click.
Now we need to create a web worker to run any function.
worker.js
// Listen for messages from the main script
self.onmessage = function(event) {
const { fn, data } = event.data;
// Execute the provided function with the given data
const result = fn(data);
// Send the result back to the main script
self.postMessage(result);
};
There is one problem with this example. You can not pass/send Functions to workers!
Workaround is to stringify your pure-function and pass it as an argument:
index.html
<!DOCTYPE html>
<html>
<head>
<title>Web Worker Example</title>
</head>
<body>
<h1>Web Worker Example</h1>
<button onclick="runWorker()">Run Web Worker</button>
<script>
function runWorker() {
// Create a new web worker
const worker = new Worker('worker.js');
// Send payload and function to the web worker
const data = [1, 2, 3, 4, 5];
// Function to be executed in the web worker
const sumArray = (arr) => {
return arr.reduce((sum, num) => sum + num, 0);
};
worker.postMessage({ payload, functionToExecute: sumArray.toString() });
}
</script>
</body>
</html>
worker.js
// Listen for messages from the main script
self.onmessage = function (event) {
const { fn, data } = event.data;
// Execute the provided function with the given data
const result = new Function("return " + fn)()(data);
// Send the result back to the main script
self.postMessage(result);
};
2) Getting rid of worker.js file (with working example)
Creating a worker.js file and importing it is a lot of work :)
Passing a file name to Worker constructor is not the only way to create a web worker. We can pass base-64 encoded javascript in it (for this example, will only encode console.log(‘’hello world’):
You can use this link to encode your custom javascript http://base64online.org/encode/
index.html
<!DOCTYPE html>
<html>
<head>
<title>Web Worker Example</title>
</head>
<body>
<h1>Web Worker Example</h1>
<button onclick="runWorker()">Run Web Worker</button>
<script>
function runWorker() {
// Create a new web worker with Encoded JS
// self.onmessage = () => console.log("hello world")
const worker = new Worker(
"data:application/x-javascript;base64,c2VsZi5vbm1lc3NhZ2UgPSAoKSA9PiBjb25zb2xlLmxvZygiaGVsbG8gd29ybGQiKQo="
);
// Send payload and function to the web worker
const data = [1, 2, 3, 4, 5];
// Function to be executed in the web worker
const sumArray = (arr) => {
return arr.reduce((sum, num) => sum + num, 0);
};
worker.postMessage({
payload: data,
functionToExecute: sumArray.toString(),
});
}
</script>
</body>
</html>
3) Executing the function and payload in the worker
Let’s modify our worker to execute our function and return a value this time, now we will encode this piece into a worker:
self.onmessage = function (event) {
const { functionToExecute, payload } = event.data;
const result = new Function("return " + functionToExecute)()(payload);
self.postMessage(result);
};
index.html
<!DOCTYPE html>
<html>
<head>
<title>Web Worker Example</title>
</head>
<body>
<h1>Web Worker Example</h1>
<button onclick="runWorker()">Run Web Worker</button>
<script>
function runWorker() {
// Create a new web worker with Encoded JS
// self.onmessage = () => console.log("hello world")
const worker = new Worker(
"data:application/x-javascript;base64,c2VsZi5vbm1lc3NhZ2UgPSBmdW5jdGlvbiAoZXZlbnQpIHsKICBjb25zdCB7IGZ1bmN0aW9uVG9FeGVjdXRlLCBwYXlsb2FkIH0gPSBldmVudC5kYXRhOwogIGNvbnN0IHJlc3VsdCA9IG5ldyBGdW5jdGlvbigicmV0dXJuICIgKyBmdW5jdGlvblRvRXhlY3V0ZSkoKShwYXlsb2FkKTsKICBzZWxmLnBvc3RNZXNzYWdlKHJlc3VsdCk7Cn07"
);
// Send payload and function to the web worker
const data = [1, 2, 3, 4, 5];
// Function to be executed in the web worker
const sumArray = (arr) => {
return arr.reduce((sum, num) => sum + num, 0);
};
worker.postMessage({
payload: data,
functionToExecute: sumArray.toString(),
});
}
</script>
</body>
</html>
Okay, now we are sending our function and payload to worker and it’s getting executed but we are not getting the result back.
4) Getting the result via promise (TLDR Working Example)
We can turn this piece into a promise based function to make it easier to use.
<!DOCTYPE html>
<html>
<head>
<title>Web Worker Example</title>
</head>
<body>
<h1>Web Worker Example</h1>
<button onclick="runWorker()">Run Web Worker</button>
<script>
// Create a new web worker
const myWorker = new Worker(
"data:application/x-javascript;base64,c2VsZi5vbm1lc3NhZ2UgPSBmdW5jdGlvbiAoZXZlbnQpIHsKICBjb25zdCB7IGZ1bmN0aW9uVG9FeGVjdXRlLCBwYXlsb2FkIH0gPSBldmVudC5kYXRhOwogIGNvbnN0IHJlc3VsdCA9IG5ldyBGdW5jdGlvbigicmV0dXJuICIgKyBmdW5jdGlvblRvRXhlY3V0ZSkoKShwYXlsb2FkKTsKICBzZWxmLnBvc3RNZXNzYWdlKHJlc3VsdCk7Cn07"
);
async function runWorker() {
const data = [1, 2, 3, 4, 5];
// Function to be executed in the web worker
const sumArray = (arr) => {
return arr.reduce((sum, num) => sum + num, 0);
};
// Execute the function in the web worker
const result = await executeFunctionInWorker(sumArray, data);
console.log("Result from web worker:", result);
}
async function executeFunctionInWorker(functionToExecute, payload) {
// Create a Promise to handle the worker's response
const workerResponse = new Promise((resolve) => {
// Listen for messages from the worker
myWorker.onmessage = function (event) {
resolve(event.data);
};
});
// Post a message to the worker with the function and data
myWorker.postMessage({
functionToExecute: functionToExecute.toString(),
payload,
});
// Wait for the worker's response
const response = await workerResponse;
return response;
}
</script>
</body>
</html>
This example demonstrates how you can utilize the executeFunctionInWorker()
function to execute a specific function in a web worker, passing data to it, and retrieve the result asynchronously.
You can take this implementation one level further and give id’s to each message and even turn it into a queue based execution worker.