Web Worker Pt. II - Heartbeat/Polling with Web Workers

In my first post about Web Workers, I was outlining what they are and what they can be used for.

You probably saw code snippets of Web Worker that loaded the worker script file like this: new Worker("script.js") and then event listeners and postMessage() calls to let the main script communicate with the Web Worker.

Using an external worker.js is awkward and kind of weird, so people came up with inlining the Web Worker code and load it with the URL.createObjectURL() method.


var blob = new Blob(['self.onmessage = '], fn.toString()], { type: 'text/javascript'});
var url = URL.createObjectURL(blob);

This is bizarre, but apparently, you can load an encapsulated object this way.

After getting the gist of how to write Web Workers, it's time to find the right micro-framework/library, because smarter people than me figured out how to streamline and simplify the process of doing this.

I found a few, many abandoned in 2015 - so apparently it didn't solve a lot of problems for people, so the demand was low. Remember, the use-case of sorting a large array or do some other expensive calculations comes up rarely.

I decided to use greenlet, because it looked really simple to implement:


let getName = greenlet( async username => {
  let url = `https://api.github.com/users/${username}`
  let res = await fetch(url)
  let profile = await res.json()
  return profile.name
})

console.log(await getName('developit'))

This script fetches the name of the developer from the git hub API in a web worker and displays it in the console.

No extra web worker script, just inline code!

Heartbeat of a Web Worker

As mentioned before the use cases for Web Workers are limited, because you can only work with plain objects and arrays. No access to DOM nodes or LocalStorage.

But I've got a tiny piece of script in a project that was polling a backend to see if it was still alive and that seemed like a perfect use case for a Web Worker to run off the main thread.

For the sake of the experiment, I'm going with a slightly extended version of a heartbeat functionality. It also uses the JavaScript Channel Messaging API1 to send messages back and forth, while the Web Worker is running.

The following script fetches https://jsonplaceholder.typicode.com/todos/${id} every 5 seconds and postMessage(text) back to the main script to update the DOM with the result.


<div id="app"></div>

let heartbeat = greenlet(async (port) => {
  // port = port2 MessageChannel
  port.onmessage = ({ data }) => {
    console.log('received `port1.postMessage(%s)`', data);
    data = data.split('').reverse().join('');
    port.postMessage(data);
  }

  const heartbeat = async () => {
    let id = Math.floor(Math.random() * (200 + 1));
    let url = `https://jsonplaceholder.typicode.com/todos/${id}`;
    let res = await fetch(url);
    let todo = await res.json();
    return todo.title;
  };

  const heartbeatIntervalId = setInterval(async () => {
    let text = await heartbeat()
    port.postMessage(text)
  },
  5000
  );
})

// https://github.com/developit/greenlet/issues/30#issuecomment-389657408
const { port1, port2 } = new MessageChannel;

heartbeat(port2).then(() => {
  // listen for a message from worker
  port1.onmessage = e => document.getElementById('app').innerHTML = e.data;
  // post a message to the worker
  port1.postMessage('Send a message to the Worker');
});

Please check out a working example on Codepen

Those ports are important to keep a MessageChannel open from the worker to the script - else you can't access the results of your worker. But this is only the case if the worker is a long-running process!

In the initial greenlet example, the worker just runs once and then comes back with the result, that can be inserted in the DOM or be used otherwise.

For a long-running process (via setInterval) you need to use MessageChannels.

Internally, greenlet is using URL.createObjectURL(blob).

That's it for my Web Worker excursion. I think on a normal website, Web Workers are not needed. In web applications, there might be a reason to evaluate them, but as long as there are no operations that freeze the browser's main thread, they are not worth the hassle.

Alternatives

I started my first experiments with WorkerB, but unlike greenlet, WorkerB relies on an external file, that calls itself.

task.js I haven't tried yet, but it looks very promising as well.

Have fun.