Learn how web workers help with web app performance, and get started by building a basic JavaScript web worker.
In 2019, the web ecosystem has evolved to the point where the browser is an execution environment for applications built on JavaScript. This is reflected in the speed with which the industry comes up with new frameworks, paradigms, module loaders and bundlers, dependency managers, build tools, and package managers year after year.
When JavaScript was conceived in the early days of the internet, the direction of internet development was not clear. Due to the constant, rapid change in the industry and ecosystem, the need for the backward-compatibility of browsers and web standards, the evolution of JavaScript became a constant stream of patches, hacks and afterthoughts.
Today’s mobile devices normally come with 8+ CPU cores, or 12+ GPU cores. Desktop and server CPUs have up to 16 cores, 32 threads, or more.
In this environment, having a dominant programming or scripting environment that is single-threaded is a bottleneck.
This means that by design, JavaScript engines — originally browsers — have one main thread of execution, and, to put it simply, process or function B cannot be executed until process or function A is finished. A web page’s UI is unresponsive to any other JavaScript processing while it is occupied with executing something — this is known as DOM blocking.
This is terribly inefficient, especially compared to other languages.
If we go to JS Bin and run this code in the browser’s JavaScript console:
//noprotect
i = 0;
while (i < 60000) {
console.log("The number is " + i);
i++;
}
… the whole jsbin.com website will become unresponsive until the browser counts — and logs — to 60,000.
We won’t be able to interact with anything on the page, because the browser is busy.
Now, this is a relatively undemanding computing process, and today’s web apps often involve much more demanding tasks.
We need to be able to compute things in the background while the user seamlessly interacts with the page.
The W3C published a first draft of the web workers standard in 2009. The full specification can be found on the Web Hypertext Application Technology Working Group website — or WHATWG — a web standards body alternative to W3C.
Web workers is an asynchronous system, or protocol, for web pages to execute tasks in the background, independently from the main thread and website UI. It is an isolated environment that is insulated from the window
object, the document
object, direct internet access and is best suited for long-running or demanding computational tasks.
Apart from web workers — a system dedicated to multithreading — there are other ways to achieve asnychronous processing in JavaScript, such as asynchronous Ajax calls, and event loop.
To demonstrate this, we will go back to JS Bin and try this snippet:
console.log("A");
setTimeout(function(){console.log("B");},2000);
console.log("C");
setTimeout(function(){console.log("D");},0);
console.log("E");
setTimeout(function(){console.log("F");},1000);
When we run this, our log sequence is A, C, E, D, F, B
. The browser first schedules operations without the timeout, as they come, and then it executes the setTimeout()
functions in the order of their respective specified delays. However, this asynchronicity should not be automatically conflated with multithreading. Depending on the host machine, this can often be just a single-thread stack of the calls in the order we explained.
As Mozilla’s JavaScript reference website explains, web workers are a “means for web content to run scripts in background threads.”
We use them in the following way: we check for the availability of the Worker()
constructor in the browser, and if it is available, we instantiate a worker object, with the script URL as the argument. This script will be executed on a separate thread.
The script must be served from the same host or domain for security reasons, and that is also the reason that web workers won’t work if we open the file locally with a file://
scheme.
if (typeof(Worker) !== "undefined") {
worker = new Worker("worker.js");
}
Now we define this code in the worker.js
file:
i = 0;
while (i < 200000) {
postMessage("Web Worker Counter: " + i);
i++;
}
An important thing to note here is the separation of the window
and document
scope of execution in the main browser window thread, and the worker
scope.
In order to make use of the worker
thread, these two scopes need to be able to communicate. To achieve this, we use the postMessage()
function within the worker.js
file — to send messages to the main browser thread — and the worker.onmessage
listener in the main thread to listen to worker
messages.
We can also send messages from the main browser thread to the worker
thread or function. The only difference is that we reverse things, and call worker.postMessage()
on the main thread, and onmessage
on the worker thread. To quote Mozilla’s developer reference:
Notice that
onmessage
andpostMessage()
need to be hung off theWorker
object when used in the main script thread, but not when used in the worker. This is because, inside the worker, the worker is effectively the global scope.
We can use theterminate()
method in the same way, to end our worker’s execution.
With all this in mind, we come to this example:
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>Web Workers Example</title>
<style type="text/css">
body {padding-top:28px;}
.output-cont {margin-left:12%; margin-top:28px;}
.output-cont h3 {width:200px; height:100%;}
.output-cont button {padding:4px 8px; font-size:1.1rem; font-family:sans-serif; }
</style>
</head>
<body>
<div class="output-cont"><button onclick="testWorker()">start worker</button><h3 id="workerOutput"></h3><button onclick="terminateWorker()">terminate worker</button></div>
<br/>
<div class="output-cont"><button onclick="testMainThread()">start blocking thread</button><h3 id="mainThreadOutput"></h3></div>
<br/>
<div class="output-cont"><button onclick="alert('browser responsive!')">test browser responsiveness</button></div>
<script>
var worker;
function testWorker() {
if (typeof(Worker) !== "undefined") {
if (typeof(worker) == "undefined") {
worker = new Worker("worker.js");
}
worker.onmessage = function(event) {
document.getElementById("workerOutput").innerHTML = event.data;
};
} else {
document.getElementById("workerOutput").innerHTML = "Web Workers are not supported in your browser";
}
}
function terminateWorker() {
worker.terminate();
worker = undefined;
}
function testMainThread() {
for (var i = 0; i < 200000; i++) {
document.getElementById("mainThreadOutput").innerHTML = "Main Thread Counter: " + i;
}
}
</script>
</body>
</html>
and worker.js:
i = 0;
while (i < 200000) {
postMessage("Web Worker Counter: " + i);
i++;
}
This gives us the opportunity to test out the effects of main-thread execution on page behavior and performance versus the web worker’s effects.
In this tutorial, we used http-server
to serve the files locally.
Now we can see that the worker thread does not block the interactivity of the main browser process, and looping through 200,000 numbers does not affect the main thread. The numbers in the #workerOutput
element are updated on every iteration.
The blocking thread, or main thread, when engaged in a loop, blocks all interactivity (we have set the number of iterations to 200,000 here, but it will be even more obvious if we increase it to 2,000,000).
One more thing that points us to a blocked main thread is that the worker process updates the page on every iteration, and the loop in the main thread (the one defined in index.html
) only updates the #mainThreadOutput
element on the last iteration.
This is because the browser is too consumed with counting (for
loop) to be able to redraw the DOM, so it does it only once its business with the for
loop is fully done (at the end of the loop).
In this article, we introduced web workers, a technology that helps the web industry keep up with more and more demanding web apps. This is done by providing a way for web apps to leverage multi-processor and multi-threaded devices by bestowing some multi-threaded superpowers to JavaScript.
Web workers turn the mobile and desktop browser environments into application platforms, providing them with a strict execution environment. This strictness may force us to provide for the copying of objects between multiple threads, and to plan our applications with these constraints in mind.
Do you have any tips regarding web workers, and the web as a programming platform? Let us know in the comments!
#javascript #web-development