Web Workers 101

Do you want to know the basic “todo mvc” of the Web Workers API? This is not the end all of what there is to know about Web Workers, but rather this a good first guide to exploring the topic. The posts on MDN would be a great resource for a detailed verbose explanation.

AAEAAQAAAAAAAAjdAAAAJGNmNDdkZjY3LWI3YTYtNDA4NS1iNTMzLTczNzA4M2IwZjVjYw.jpg

Source: http://dilbert.com/strip/2011-12-16

The Web Workers API allows us to run scripts in the background independent of any user interface scripts that we are currently running (or will run in the future). Think of it as the ever listening phone in the comic above, it doesn’t interrupt the Pointy-haired Boss until it has done the work asked.

Web Workers as specified by the WHATWG HTML Living Standard are:

Workers utilize thread-like message passing to achieve parallelism
- Eric Bidelman

Use Cases #

The Web Workers API #

Basic Creation #

Web Workers are created by calling the constructor method of a Worker().

const aURL = 'worker.js';
let myWorker = null;

if (window.Worker) {
  myWorker = new Worker(aURL);
}

A URL is provided as an argument which represents the URL of the script the worker will execute. It must obey the same-origin policy.

The constructor returns the dedicated worker instance created. Here you can attach listening events as well as use the postMessage() method to communicate with the Worker. The message passed to the Worker could be any value or object handled by the structured clone algorithm, which includes cyclical references.

// app.js

const myWorker = new Worker('worker.js');

myWorker.addEventListener('message', (e) => {
  console.log(`[Worker RESPONSE] : ${e.data}`);
}, false);

myWorker.addEventListener('error', (e) => {
  console.log(`[Worker ERROR] : Line ${e.lineno} in ${e.filename} : ${e.message}`);
}, false);

myWorker.postMessage('Harry Potter');

We try here a very basic example of a worker doing some String manipulation before returning it back to the caller. Basically passing in a name and returns a Harry-Potter-esque book title (e.g. passing in “Chris” could return “Chris and the Goblet of Fire”).

// worker.js

onmessage = function onMessageReceived(e) {
  const titles = [
    'Philosopher\'s Stone',
    'Chamber of Secrets',
    'Prisoner of Azkaban',
    'Goblet of Fire',
    'Order of the Phoenix',
    'Half-Blood Prince',
    'Deathly Hallows'
  ];
  const randomTitle = Math.floor(Math.random() * titles.length);
  postMessage(`${e.data} and the ${randomTitle}`);
}

Inlining Creation #

If you remember from the section above, creating a Worker requires passing in a URL that respects the same-origin policy. It is noted in the (WHATWG HTML Living Standard that “any same-origin URL (including blob: URLs) can be used. data: URLs can also be used, but they create a worker with an opaque origin.”

So this opens up the possibility of creating Web Workers on the fly without specifying an additional file using blobs (see https://www.linkedin.com/pulse/using-urlcreateobjecturl-chris-ng on using the createObjectURL function).

const blob = new Blob([
    "onmessage ​= function onMessage(e) { postMessage('Message Recieved.'); }"]);

const blobURL = window.URL.createObjectURL(blob);

const myWorker = new Worker(blobURL);

myWorker.onmessage = function onMessage(e) {
  console.log(e.data);
}

myWorker.postMessage(); // Message Recieved.

In a Chrome browser, there is an easy way to view all of the created blob URLs at chrome://blob-internals/.

data: URLs create a worker with an opaque origin. Both the constructor origin and constructor url are compared so the same data: URL can be used within an origin to get to the same SharedWorkerGlobalScope object, but cannot be used to bypass the same origin restriction.
/- HTML WHATWG Spec

Functions and interfaces available in workers #

See the full list at the ever useful MDN.

Shared Workers #

A dedicated worker (all the examples above) is only accessible from the script that first spawned it, whereas Shared Workers can be accessed from multiple contexts such as browser windows, other Web Workers, or even iframes.

Shared Workers are still created in the same way using the constructor pattern new SharedWorker(). However a Shared Worker is accessed by using a MessagePort, each Shared Worker has its own dedicated MessagePort to route messages from different contexts. So as you can see in the example below, unlike Dedicated Web Workers, you must access the Shared Worker’s methods using the port property. The rest is more or less the same.

// app.js

const mySharedWorker = new SharedWorker('SharedWorker.js');


mySharedWorker.addEventListener('message', (e) => {
  console.log(`[Shared Worker] : ${e.data}`);
}, false);

// This step is not needed if using the onmessage property
mySharedWorker.port.start();

mySharedWorker.port.postMessage(this.location.host);

FYI: Starting the port is only needed if you are binding event listeners using the addEventListener pattern, binding a function to the onmessage property implies the start of the Shared Worker’s port.

// SharedWorker.js

onconnect ​= function onConnect(e) {
  const port = e.ports[0];

  port.onmessage = function onMessage(e) {
    // Do something with the message here
    port.postMessage(`Message Received From ${e.data}`);
  }
}

Inside the SharedWorker.js code, we use the SharedWorkerGlobalScope.onconnect functionality to connect to the same port discussed above. After we have successfully connected, we then use MessagePort start() method to start the port and the onmessage handler to deal with messages sent from the main threads.

Our example above would post back that it received a message from a particular host to all the listeners to the Shared Worker. The benefit here is that you can maintain the data until the last connection has been ended (remember Shared Workers are accessible across tabs and different contexts).

Security #

Web Workers introduces a security concern for your applications because you are trusting an outside source to modify your application’s UI and/or logic that is why there are strict Same-Origin restrictions to resources you pull in:

Web Workers however are not bound by the content security policy of the document (or of the parent Web Worker) since Web Workers have their own execution context distinct from that of its origin.

Web Workers Versus Service Workers #

Service Workers act as proxy servers that sit between web applications, and the browser and network (when available). It is ideally used to deal with offline situations, background syncs, or push notifications.

While Web Workers can be used to act in a similar fashion such as performing a Web I/O via the main app script while also off loading the I/O to the Worker at the same time, it does not have access to domain-wide events such as network fetches in order to act as a proxy.

Both Service Workers and Web Workers utilize the postMessage method to handle communication with the main application. So it is not surprising either that both cannot directly interact with the DOM.

Compatibility #

Web Workers are surprisingly very well supported across the spectrum with basic features being available across all modern browsers except for Opera Mini.

Screen Shot 2017-01-15 at 5.57.05 PM.png

However Shared Web Workers are still only supported by Chrome, Firefox, and Opera at the time of writing.

What’s Next? #

This blog post barely scratches the surface of what Web Workers can do for us. There are still plenty of things to explore in performance tuning applications with the help of Web Workers.

Parashuram a software engineer at Microsoft showcased how Web Workers can be a rendering tool for a React-based application at last year’s React Rally 2016 (my recap blog on the conference). He created a site http://web-perf.github.io/react-perf/ which measures the scripting time the Virtual DOM takes. His solution is to use Web Workers to reduce scripting time by creating a custom React Renderer (ReactWorker) which does the ProxyDOM on the Web Worker message bus it to the UI thread that then modifies the DOM.

Funny enough I remember Parashuram talking about how he tried to increase the throughput between postMessages from the app and the Web Worker by trying different techniques such as using transferable and superpack (compression library). Well it turns out that Plain-Old-JSON-Stringify() (POJS?) was the most efficient solution!

How do you use Web Workers in your application? What do you envision would be the future use cases of this rapidly maturing technology? And is it feasible to start using it today with wide support for basic features?

This blog entry was originally published at my LinkedIn page.
See it at Web Workers 101.

 
21
Kudos
 
21
Kudos

Now read this

Angular + React = Performance

Angular and performance. When you do a Google search of these two words together, what you get it most likely blog posts about fixing performance issues or ways to avoid pitfalls. Theres no hiding the fact that the not-so-secret secret... Continue →