Here we go again, another addition to the ever-growing list of “things I need to know about to be a Web developer.” This one isn't just another new Web feature: This one's going to let you do things you really couldn't do before. Do I have your attention yet? Of all the cool things we've been able to accomplish in Web development, we've haven't been able to do real multi-threaded development. I'm not talking about non-blocking tasks. I'm talking about real concurrency. In .NET, this isn't anything new. But JavaScript has been a bit behind in this.
Not anymore. Hello Web Workers. To be clear, Web Workers are not exactly brand new. I found a draft specification by the W3C dating back to 2015. But I'm amazed by how many Web devs I've spoken to who've either never used them or never heard of them ? yours truly included, although they're part of the HTML5 spec. Briefly talking about them leads to the misunderstanding that they are a little complicated to get your head around, so let's dive a little deeper.
In a Nutshell
Let me start telling you specifically where in the stack they reside: in the client, plain and simple. I say this to make sure you understand that Web Workers are NOT a server-side technology that you kick off from the client, because I've been in conversations where I noticed that immediate misunderstanding. Web Workers are stored, created, and executed on the client.
If I can define their primary purpose in a nutshell, I'd say that Web Workers allow you to perform multi-threading in your Web applications. I mean true
multi-threading, meaning true
concurrency, not just non-blocking asynchronous tasks. Web Workers are scripts initiated from the JavaScript code in your application and execute on a thread separate from that of your application. Any communication between a Web Worker and your application occurs through events. For the most part, they're a place to execute code, away from your application and without blocking your application. If you're not new to the concept of messaging patterns, you're going to feel right at home. If you are, fear not: Web Workers will be a simple, perfect introduction for you. For the most part, the most complex part of them is understanding what you CANNOT do in them. Understanding their limitations is crucial to determining what your application does now that you can potentially spawn off to a Web Worker. Web Workers are written entirely in JavaScript, so if you're not a JavaScript developer, become one ? today. Like it or not, JavaScript is no longer a mere scripting language. It's a complete head-to-head competitor with languages like C# and Java, and has been for some time now.
Web Workers allow you to perform multi-threading in your Web applications.
I'm going to take a different approach at teaching you this from what I've seen before. As opposed to giving you every detail up front in a linear way, I'm going to go really simple first, then add complexity bit by bit, each time starting from the beginning. I guess you call this “agile.” My application code for now, with the exception of the Web Worker, will be in a JavaScript file called App.js
in a folder called Scripts
. Refer to the sidebar for details on how I'm doing the hosting. I'll also include the code-location in the sidebar.
What's in a Script
The actual Web Worker is just a JavaScript code construct and is written just like every other JavaScript you've created in the past. An immediate difference is in the way it's loaded. A Web Worker script is loaded on demand, at run-time. Unlike other JavaScript files with which you've worked in the past that have been loaded into either a <script>
tag with an assisted resource or during a transpilation or pseudo-compilation process of some kind, Web Workers scripts are loaded and initiated using an actual JavaScript command.
var myWorker = new Worker('Scripts/my-first-worker.js');
I'll get into this more as the article continues but I should tell you that this Worker
object you see here is what's called a Dedicated Worker. I'll use this one for a while before getting into another type called a Shared Worker. Instantiating the Worker
object loads and executes the script, much the same way a <script>
tag would. The cool thing is that it executes it on a separate thread from the one in which your application is running. The path in the string value is relative to the root of your application. A Dedicated Web Worker is accessible only from the script that loaded it, meaning that if another script starts it up, it will be another instance of the Web Worker.
Now for the challenging corner into which your fearless writer has programmed himself: the first thing you cannot do in a Web Worker. See, ordinarily to show you a script is running; I'd just put an alert command in it in order to pop up an alert box. But remember I said that the script I just kicked off is going to execute on another thread, one that does NOT have access to the UI of the application. For the time being, you're just going to have to trust me that as I continue, I'll prove to you that in the snippet above, I indeed kicked off a script. More often than not, you're going to start a Web Worker in order to run some code that may perhaps take enough time that it merits not wanting to tie up your application. Even though the line above starts the script, you don't usually put the code you want executed in there by itself. Here is where events come in.
Event-Based Communication
Events are how your application and the Web Worker script communicate with each other. When you start a Web Worker, you register
event listeners in your application. A Web Worker can publish an event of the same name, executing the code in your registration back in your main application code. Similarly, the Web Worker registers listeners for events that your application can publish. In both directions, the publisher can send data through an event.
Data sent to a Web Worker from or to an application from a Web Worker has some limitations. Data transferred through an event is crossing a barrier within your browser that requires serialization, but no worries; this is taken care of automatically. It does impose a limit on what you can send or receive through an event. This means that any object you send or receive must be serializable. Methods are not allowed as part of event data. Although this makes sense and may seem like it's not a big deal for you, you'd be surprised by how this limitation helps you uncover non-serializable objects you've been using in the past as you attempt to transfer them to a Web Worker. On that note, let's see exactly how you do that.
As I said earlier, after you create the Web Worker, your next step is to wire an event so it can receive a message from your application. In the Web Worker script, I can write:
onmessage = function(e) {
console.log('Message received from main script: ' + e.data);
}
Note that the data you send the Web Worker comes in the data
property of the event
function argument. So now, back in the application, I can write:
myWorker.postMessage('Hello worker');
That data
property you just saw contains the text Hello worker. This hard-coded string can be any object you want, so long as it is serializable ? but you know this already.
Data of course, is optional, depending on need. The important thing here is that the application just ordered the Web Worker to do something. All the current implementation is doing right now is writing out the received data to the console; things will get more exciting soon, don't worry. For right now, let's talk about debugging.
If an uncaught error occurs in the Web Worker, the main app can wire an event to catch it.
myWorker.onerror = function (e) {
console.log(e);
alert(e.message);
}
Among several other things, the message
provides a human-readable error message that you can report. At this point, it's up to the app to do anything further.
Debugging
I'm going to assume that, like me, you're using Google Chrome as your browser, so, I'm going to speak in that language. In a normal application, you can go to the Sources
tab in the Chrome Developer Tools (F12
) and see your loaded code tree. Here, you can find all your loaded JavaScript files and set whatever breakpoints you need. If you do that with what I've written so far, you'd see my aforementioned Scripts
folder and the App.js
file. But where oh where is the my-first-worker.js
file?
If you look at the source tree on the left in the developer tools window, you'll notice that the worker
file is in a tree node sibling to the application host, NOT in the Scripts
folder (see Figure 1).
Signaling Completion
After Web Worker is finished executing the code in the event published from the application, you may want to know about it. Remember, the only way the application and a Web Worker can communicate is through events and you already know how to do this. No, I mean you literally know exactly how to accomplish this because it's exactly the same as what I did earlier, when I fired an event from the application to the Web Worker. This time, the Web Worker publishes an event for which the application will be listening. Let's modify the Web Worker event you saw earlier first:
onmessage = function(e) {
console.log('Message received from main script: ' + e.data);
console.log('Posting message back to application');
postMessage('Back atcha');
}
Now I'm going to register an event handler back at the application.
myWorker.onmessage = function (e) {
alert(e.data);
}
As you may have already guessed, e.data
contains the string Back atcha, but it could have been any serializable
object.
The most important takeaway from this exercise is that you're executing asynchronous code here. This isn't something new to Web developers but typically, they encounter this when making HTTP calls to a service. Here you're seeing this all in the client as, in fact, you've written a multi-threaded client application.
Complex Messages
The examples I've shown you are simple, as they were meant to be. The reality is that more often than not, you'll need to send a little more than just a string between the application and the worker in both directions. But, as I said earlier, a message can be any serializable value. So, you can easily send an object, giving it a property that identifies the message, giving the receiving side something to condition on.
postMessage({
name: 'StartProcess1',
text: 'Back atcha'
});
This technique works on the postMessage
function used in the application and/or the worker. Although the shape of the object that makes up a message is free form, it's a good practice to establish a consistent model that your application and Web Workers use across the board. If you, at minimum, follow a similar pattern to what I'm showing you here, you can condition on the name
property of the event message; again, on both ends, the application and/or the worker.
if (e.data.name) {
switch (e.data.name) {
case 'StartProcess1':break;
}
}
Even if your worker has a single purpose (not likely), this pattern will set you up for any future enhancements. And because this is merely a coding pattern, you're going to find it useful not only in the type of Web Worker I've shown you so far, but also the type I'm going to show you next.
Debugging
Debugging a Dedicated Web Worker is just a little different than any other script you have. You'll see the Web Worker in the source code tree available in the Sources
tab of the Chrome developer tools. The difference is that you won't see it in the folder where you find the script that spun up the Web Worker. Instead, you'll see it hung off the top-level node but you'll recognize the Web Worker by the name of its script. In Figure 2, you can see my-first-worker.js
in the code tree's bottom-left. This figure will be used again later in the article, so don't concentrate on other weird things you see in the code tree. You can place breakpoints here the same as you're used to doing in other scripts.
If the Web Worker loads sub-scripts or is an inline worker, the debugging changes just a bit. I'll cover this slight variation at the end in Advanced Scenarios.
When a Dedicated Web Worker is initiated from more than one script, things will get a little visually confusing. The code tree shows more than one instance of the Web Worker. If the Web Worker has sub-scripts or inline scripts (discussed later), it will look even more confusing, as will be shown in Figure 5 later. Don't let this scare you. Even though it will seem nearly impossible to know which version got instantiated from what script, you'll notice that if you set a breakpoint and switch to another version of the Web Worker in the code tree, you'll see the breakpoint there as well, so simply choose one.
Shared Workers
What you've seen so far is one version of a Web Worker, known as a Dedicated Worker. This worker is only accessible from the JavaScript script that initiated it. If another script file initiates the Web Worker's script, it will be setting up a completely different Web Worker in a different thread or execution context. Variables used in one instance of the Web Worker store values that are limited to the Web Worker instance where it was set up. There's another kind of Web Worker that adds a steroid dose to the situation, called a Shared Worker. Although the Dedicated Worker is great for kicking off some functionality to another thread, it has its limitations. This also makes it limited to one browser instance of the application in question. If you run another instance of the same application, it uses another instance of the worker script just as if you started the Web Worker from another page using another script. This is the code I've shown you so far. So, let's change up the syntax a little.
The Syntax Changes
The first thing that's different is the way the Web Worker script is loaded. Before, it was by instantiating the Worker
object: the Shared Worker script is loaded by instantiating the - you guessed it - SharedWorker
object.
var mySharedWorker = new SharedWorker('Scripts/my-shared-worker.js');
Simple enough so far? Well, it gets a little more complex. Unlike a Dedicated Worker, further actions aren't performed from the variable I just created; but instead are performed from a port
property. On the side of the application, the port
property is a property of the instantiated variable, mySharedWorker
in this case.
mySharedWorker.port.postMessage('Hello Worker');
On the side of the Web Worker, the port is obtained a little differently. See, the idea here is that there are multiple instantiators of a worker in a single application, whether running in the same or in multiple browser instances, but all are sharing the same Web Worker. Unlike a Dedicated Worker, a Shared Worker needs to listen for the connect
event first in order to obtain the port that just connected.
onconnect = function (e) {
const port = e.ports[0];
It's on that port that you can then listen for messages. In case you're wondering, in all the times I've worked with Shared Workers, I've never seen that array contain more than one item.
port.onmessage = function (e) {
And of course, it's recommended that you use the same message patterns I already showed you so you can identify the message and set up a condition for different ones.
Now, remember that every script in your application that instantiated the Web Worker, or every browser instance of your application, will all be connecting to this same worker in the same fashion, but each one will result in a different value for the port
variable. This means it's up to you to track all the connected ports. This way, your Shared Worker can send messages that will be received by all the connected ports, meaning all the instances of your application.
Tracking Connections
Because the Shared Worker is the same for all of the connected application instances, you can simply have a variable that stores a port when it's set up.
var connections = [];
Now I can check to see if a port that just connected is in that array and if not, I can add it in the code.
const existingConnection = connections.find(connection => {
return connection === port;
});
if (existingConnection === undefined || existingConnection === null)
connections.push(port);
Now you can loop through the connections
array to send a message to all connected application instances. Let's take a look at the completed code and I'll explain the order of operations.
First, the application code.
var mySharedWorker = new SharedWorker('Scripts/my-shared-worker.js');
mySharedWorker.port.onmessage = function (e) {
alert(e.data.text + ' received from message '+ e.data.name);
}
mySharedWorker.port.postMessage({
name: 'StartProcess1',
text: 'And here we go...'
});
The worker is instantiated and a message is set up. Obviously, if I wanted to fancy it up, I'd case (switch) on e.data.name
. Then I'm posting a message called StartProcess1
to the worker.
See the complete Web Worker code in Listing 1.
Listing 1: Shared Worker complete code
var connections = [];
onconnect = function(e) {
const port = e.ports[0];
const existingConnection = connections.find(connection => {
return connection === port;
});
if (existingConnection === undefined || existingConnection == null)
connections.push(port);
port.onmessage = function(e) {
console.log('Message received from main script: ' + e.data);
if (e.data.name) {
switch (e.data.name) {
case 'StartProcess1':
console.log('Posting message back to application instance that called me.');
const msg = {
name: 'ReturnFromProcess1',
text: 'Back atcha'
};
port.postMessage(msg);
break;
case 'NotifyAll':
console.log('Posting back to all application instances.');
connections.forEach(function(connection) {
const msg = {
name: 'NotificationForAll',
text: 'Hello everyone!'
};
connection.postMessage(msg);
});
break;
}
}
console.log('Posting back to all application instances.');
connections.forEach(function(connection) {
const msg = {
name: 'ReturnFromProcess1',
text: 'Back atcha'
};
connection.postMessage(msg);
});
};
};
As you can see in the code listing, after the connections array is created, the first wiring is the connect listener and on that listener is where the message listener is wired. As application instances connect, their port is added to the array after ensuring they're not in there yet. When the application sends the worker an event named StartProcess1
, a message named ReturnFromProcess1
is sent back to that same port, meaning that same application instance. An event named NotifyAll
sent from an application instance causes a message named NotificationForAll
to be sent to all connected application instances. The mySharedWorker.port.onMessage
statement back at the application receives that message on each of the application instances.
As you can see, keeping track of the application connections to a Shared Worker isn't that difficult and is, in fact, more a matter of patterns than it is technology.
Debugging
Debugging a Shared Web Worker is different from a Dedicated Web Worker in that you won't see the Web Worker in the code tree of the Sources
tab of the Chrome developer tools. Instead, you have to open a new browser tab and navigate to chrome://inspect. The Dev Tools list on the left shows Shared Workers. When you click on it, the view that appears on the right offers an inspect
link, and clicking it opens a window showing your Web Worker code. This will look like the familiar code window that Chrome offers where you can proceed to set breakpoints as desired.
In the case of both a Dedicated Worker and a Shared Worker, there are still some limitations of what kind of code you can put in them. You already know that you can only send serializable objects to and from a Web Worker, so let's explore what else you can't do.
What Else Can't They Do?
It's a little strange teaching a technology and harping so much on what it can't do, but in the case of Web Workers, it's an important fact. This is because they're written in regular JavaScript, and on the surface it's easy to make the assumption that you can do anything you're used to doing in regular JavaScript. The reality is that there are certain elements to which you simply do not have access.
DOM
The first and probably the most important thing to know about is the inability to access the Document Object Model, or DOM, but it actually makes sense. Ordinarily, when you access visual elements with JavaScript code using the DOM, you're accessing elements on the HTML page in which the JavaScript code runs, and on the same thread. Remember that Web Workers execute on a separate thread from that of your application. So it makes sense that the JavaScript code that makes it up lacks access to visual elements that are outside its immediate reach.
This limitation is pretty easy to deal with because you really don't. There's no way to program a Web Worker with a way to access the DOM so there is really no way to deal with it other than to accept the fact that you just can't do it. That being said, I can hear some of you yelling at me already. And that just means that you've been reading this article and absorbing it very well. See, just because the Web Worker can't directly access the DOM, that doesn't mean it can't perform other functionality that should result in some manifestation on the page. If that's the intent and the need, you can fire an event from the Web Worker like I showed you already. In the application, an event listener wired to that event can access the DOM just fine. And of course, your event can carry an object with whatever data you may possibly need to update the UI.
Binding Libraries
In today's HTML/JavaScript applications, most of you use some kind of framework to structure and build your application. Frameworks like Angular, Vue, React, and Knockout all incorporate some flavor of a binding mechanism in order to make changes to the UI by changing state in the JavaScript. Originally, this pattern was generally referred to as MVVM, although it has since taken on different nomenclatures depending on the framework being used.
You can't access your framework's features from a Web Worker because you don't have access to its libraries; remember, they're loaded by your application and the Web Worker sits by itself in an isolated script file, running in an isolated context. Again, you rely on event communication. Just like I described earlier that you can fire an event from a Web Worker and access the DOM from that event's listener in your app, you can do the same with any framework functionality. The Web Worker performs its functionality and fires off a completion
event. Back in the application, you receive the event and modify whatever state you need that, in turn, manifests something on the UI, essentially the kind of thing you've been doing for a long time. In my opinion, what may, on the surface, seem like a limitation does, in fact, force you to keep a good separation of concerns and a good application architecture. Try to remember back when you were learning application architecture and someone told you that one of the first rules is to keep UI code out of your business tier. With this comparison, think of a Web Worker as an element of another layer of your application. It's a layer sitting on your UI tier, but a separate layer nonetheless.
Global Namespace
The window
namespace is not accessible from a Web Worker but the concept of it is, in a way. The global scope concept exists in a Web Worker by way of the self
object, although optional to use. Commands that you have learned so far, like onmessage
and postmessage
, hang off of the self
object, but as you can see, you haven't needed to specify it. The fact is, the self
object accesses one of two scope
object types: DedicatedWorkerGlobalScope
or SharedWorkerGlobalScope
. Which is used depends on whether you're using a Dedicated Worker or a Shared Worker. I'll discuss other functions that hang off of the global scope later in Additional Topics.
Advanced Scenarios
Web Workers have the ability to perform functionality that you're already used to performing in real-world applications. You've already learned that you have some limitations here and there but, in some respects, Web Workers remain as capable as any other JavaScript scripts.
HTTP
Making an HTTP call is perhaps the single most common task performed by line-of-business JavaScript-based Web applications today. The ability to access and update data is paramount to most systems we develop on a regular basis. I'm happy to tell you that the ability to do so doesn't fall into the “things a Web Worker cannot do” category.
Now that you're in a happier mood, it may be the time to tell you that you have to resort to the good old-fashioned XMLHttpRequest
object. Remember, you cannot use the new and improved JavaScript frameworks in use today, so you have to go old-school. I'm sure you agree that a refresher on how the sausage is made can't hurt.
The code sequence of this is one with which you're now familiar. The app has a spun-up Web Worker and sets up the code to call into the Web Worker with a message telling it to get some data, along with meta-information on what to get.
var myWorker = new Worker('Scripts/my-first-worker.js');
myWorker.postMessage({
name: 'getDataRequest',
request: {
verb: 'GET',
url: 'sampledata.json'
}
});
The Web Worker will, in turn, have an event handler that conditions on information in the received message, determining that it needs to go get some data. I'm going to keep this very simple, so the data I'm going to get is from a simple JSON file.
{
"name": {
"first": "Miguel",
"last": "Castro"
},
"articles": [{
"title": "Demysifying Microservice Architecture",
"publishedOn": "Jan 2018"
},
{
"title": "Understanding & Using Web Workers",
"publishedOn": "Jan 2021"
}
]
}
The details of how to use XMLHttpRequest
is beyond the scope of this article but there's plenty of information on the Web about it. The part to concentrate on is in the readyState
condition where I parse the text response and post it in a message back to the application, as shown in Listing 2.
Listing 2: XMLHttpRequest usage example
onmessage = function(e) {
switch (e.data.name) {
case 'getDataRequest': {
let request = new XMLHttpRequest();
request.onreadystatechange = () => {
if (request.readyState === 4) {
let result = JSON.parse(request.responseText);
// call app back
postMessage({
name: 'getDataResponse',
data: result
});
}
}
request.open(e.data.request.verb, e.data.request.url, true);
request.send();
}
break;
}
}
In the interest of applying some good code conventions to all this, you can see that the event name I've sent to the Web Worker is getDataRequest
(as shown two snippets above) and the event sent back to the app is named getDataResponse
. Keep in mind that the structure of this message is my design.
Back at the app, I'm going to check for the message name I'm looking for and write out the contents of the data to the console so I can examine it in the debugger.
myWorker.onmessage = function (e) {
if (e.data.name && e.data.name === 'getDataResponse') {
console.log(e.data);
}
}
Yeah, yeah, I should have used a switch
statement like I did in the Web Worker. I got lazy. When I look in the browser console, I see what's in Figure 3.
The great thing about this simple, but not trivial, example is that immediately after the message named getDataRequest
is posted from the app to the Web Worker, the code execution continues on the main app. You can do anything you want while the Web Worker carries out its instructions. The two things are running on two totally separate threads. When the Web Worker is done and sends the message named getDataResponse
back to the main app, the event in the app executes. The consequence of this is complete concurrency, as the Web Worker carries out its functionality at the same time the main application runs its own code. The events between the two is what keeps order through communication.
An important thing to note is that the JavaScript in the main app that kicked off the Web Worker and is set up to receive events needs to be kept in scope. This means that it shouldn't be an HTML page that the user may navigate completely away from while the Web Worker executes some functionality that will eventually post back to the main app. Doing so leaves nowhere for the Web Worker to post back to. But in today's single page application (SPA) architectures, that's not very difficult to handle because an application is typically structured in such a way that there's a top-level view that contains code whose scope is kept despite application navigation. There's also the concept of application services that are kept around in singleton fashion for use in many places. Both of these can be places in which to handle Web Worker. You may choose to have Web Worker and its event handlers remain visible only while on a certain view and go away on purpose if the user navigates away. All of this is up to you and your application design.
Sub-Scripts
You can break up parts of a Web Worker in separate script files for code organization or reuse. The importScripts
command brings in a script file whose functions can be called throughout the Web Worker. Perhaps I'm going to reuse the HTTP functionality I just showed you from multiple places. I may want to have a general use HTTP function that I can call and I may want that in a separate file so I can leverage it from other applications. See the data-worker.js
file in Listing 3.
Listing 3: data-worker subscript code
var doHttp = function (url, verb, callback) {
let request = new XMLHttpRequest();
request.onreadystatechange = () => {
if (request.readyState === 4) {
let result = JSON.parse(request.responseText);
if (callback)
callback(result);
}
}
request.open(verb, url, true);
request.send();
}
Now, I can modify the Web Worker to import the script up on top,
importScripts('data-worker.js');
Then, I modify the innards of the getDataRequest
case to use the new function.
case 'getDataRequest':
{
doHttp(e.data.request.url, e.data.request.verb, result => {
postMessage({
name: 'getDataResponse',
data: result
});
});
}
break;
Simple enough. Now I can make as many calls to doHttp
as I need without having to repeat the XMLHttpRequest
code. Of course, if this were a real-world application, doHttp
would handle headers, body, security, etc.
Debugging sub-scripts just involves finding them in the code tree of the Sources
tab of the Chrome developer tools. The Web Worker script in the code tree of the Sources
tab is found where you learned earlier, but now you see a node you can open further. As you do so, you find the Web Worker script again with the sub-script as a sibling, as shown in Figure 4. Now you can debug it as normal. If you have multiple instances of the Web Worker in play, you'll see multiple instances of the sub-script as well, as also shown in Figure 4. The breakpoints you set carry from one to the other, so simply chose one.
Inline Workers
Inline workers are the exact opposite of sub-scripts. There's a way to declare Web Worker code without putting that code in a separate script file. This is achieved by creating a Blob
object. This is a way to turn JavaScript code into sort of a virtual URL that you can then use in the place of a URL argument, when you create the Worker
object.
Let's say you want to create a simple worker that contains this code:
onmessage = function(e) {
console.log('Message received from main script: ' + JSON.stringify(e.data));
}
Instead of creating a separate Web Worker script, you can do the following:
var blob = new Blob([
`onmessage = function(e) {
console.log('Message received from main script: ' + JSON.stringify(e.data));
}`
]);
var blobURL = window.URL.createObjectURL(blob);
var myInlineWorker = new Worker(blobURL);
myInlineWorker.postMessage({ name: 'test1', text: 'hello inline worker'} );
Notice that the quotes surrounding the code in the snippet are under the tilde mark on the keyboard over there to the left of the 1. This is important if you're going to write code in multiple lines. When you execute this code, the console will show:
Message received from main script: {"name":"test1","text":"hello inline worker"}
I've achieved the exact same thing as I have before but without an extra script file for the Web Worker. In fact, in my sample app, I added this as a second Web Worker. You cannot declare a shared worker inline as this feature is only for dedicated workers.
Debugging inline workers is, for the most part, just like debugging any other dedicated worker. I say “for the most part” because what differs is how you locate the worker code in the browser's developer tools. You'll find the worker in the source code tree within the Sources
tab of the developer tools; but not exactly where you would expect it within the tree. Figure 5 shows the tree node that represents the inline Web Worker and you can see that it has the appearance of being dynamically created - and it was. This is important because if you set a break-point and then refresh your browser, causing the Web Worker to get “dynamically” created again, this node will have changed and you'll lose your break-point. If you have multiple instances of the Web Worker in play, you'll see multiple instances of the inline script as well, as also shown in Figure 5. But the breakpoints you set will carry from one to the other, so simply chose one.
Conclusion
In this article, I've touched on what I feel are the most important points you need to get up and running with Web Workers quickly. In fact, it covers most of what you'll ever use. Like many things in the world of JavaScript, a lot of it leaves it to you to establish good coding patterns and practices. For example, between a Web Worker and an application, there's just a message and it will be up to you to provide that message with a shape that can be beneficial to conditions. Such is the pattern I showed you where I gave each event a “name.” The way I track connections in a Shared Web Worker is another example of a pattern I chose to use.
Web Workers in their simplest fashion can be extremely beneficial as a regular Dedicated Web Worker lets you achieve something that in other platforms, such as C#, we do often and take for granted just as often: spin up threads and perform concurrent actions. Being able to do this clearly and cleanly in JavaScript is a big deal because, as I said at the beginning of this article, non-blocking asynchronous tasks are different from true concurrency.
With some creativity, I'm sure you can come up with a lot of great uses for Web Workers. One of my recent achievements has been to be able to have multiple browser instances (tabs) of a single application running but only one of them enabled with the ability to connect to a certain third-party provider service that didn't support multiple applications logged into it. Using a Shared Web Worker in conjunction with LocalStorage
and SessionStorage
, I was able to not only support this but to detect when the “connected” browser tab closed and automatically connect one of the others to take its place. If you feel you've accomplished something cool and unique in this arena, I welcome you to shoot me an email and share your story.