Simplify Web Worker code with Comlink

June 02, 2018 0 Comments

Simplify Web Worker code with Comlink

 

 

It’s not a fantasy, we can make it a reality with Comlink. Comlink is a small (1.6k) RPC library for workers. This guy made it.

Surma

His name is Surma, he's a Google colleague of mine. Comlink provides an RPC layer to call methods from a worker on the main thread. Now, it’s not the exact same code above, but it’s pretty darn close.

importScripts('./comlink.global.min.js');
const service = { double: (value) => value * 2
};
Comlink.expose(service, self);

async function init() { const worker = new Worker('./worker.js'); const service = Comlink.proxy(worker); const doubled = await service.double(2); console.log(doubled);
}
init();

Comlink exposes the service in the worker thread. On the main thread Comlink proxies the worker to communicate with the exposed service.

Comlink acts like a generated switch statement. Event listeners are set up through the expose method. When Comlink proxies a worker it creates an object that understands how to send messages to the worker thread when methods are called.

It handles the mental and code complexity of communicating with workers. Comlink states that it’s goal is: “to make WebWorkers enjoyable.” I would say it does exactly that.

Comlink isn’t just for simple tasks like above. It works quite well in more advanced usages.

Comlink handles simple return values, but what about callback functions? The Firebase SDK is chock-full of callback functions. Let’s setup the worker to import Firebase and listen to a collection restaurants from the Firestore database.

importScripts('https://cdn.jsdelivr.net/npm/comlinkjs/comlink.global.min.js');
importScripts('https://www.gstatic.com/firebasejs/5.0.4/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/5.0.4/firebase-firestore.js'); const app = firebase.initializeApp({ /* config */ });
const firestore = app.firestore(); const restaurants = { subscribe(callback) { const restaurantsCol = firestore.collection('restaurants'); restaurantsCol.onSnapshot(snap => { // unwrap the data from the snapshot callback(snap.docs.map(d => d.data())); }); }
}; Comlink.expose(restaurants, self);

I can now listen to the Firestore listener exposed to Comlink.

async function subscribe(callback) { const worker = new Worker('./worker.js'); const restaurants = Comlink.proxy(worker); restaurants.subscribe(callback);
}
subscribe(restaurants => console.log(restaurants));

This looks great! But there’s a problem. The code above won’t work. It will give the following error:

Uncaught (in promise) DOMException: Failed to execute 'postMessage' on 'Worker': restaurants => console.log(restaurants) could not be cloned.

Functions don’t work with the structured clone algorithm. Does this mean we can’t use callbacks with workers? No. Comlink has us covered.

Comlink has a special method named proxyValue. This method works around the limitations of structured clone by doing some magic with MessageChannels. Honestly, I don’t completely understand it, so I’m not going to try to explain any more ¯_(ツ)_/¯.

async function subscribe(callback) { const worker = new Worker('./worker.js'); const restaurants = Comlink.proxy(worker); restaurants.subscribe(Comlink.proxyValue(callback));
}
subscribe(restaurants => console.log(restaurants));

By using the proxyValue method we can pass callback functions that are processed on the other thread. If that’s not magic, I don’t know what is.

Workers are great, but managing them? Not so much. Comlink is a big upgrade from dealing with postMessage and the message event directly. It allows you to act as if you can call methods directly from other threads. The magic in Comlink is that it hides the worker communication from you and. It gives you the API you wish you had all along.

If you’re ready to move work off the main thread, give Comlink a try.

Want more content like this? I'm starting a newsletter! Sign up here!


Tag cloud