Use Web Workers in less than 10 minutes

javascript
web-workers
vite
by Tim Havlicek
9/17/2023

In theory, web workers are a great idea to keep the main thread of your web application free for user interaction. In practice, they are a pain to set up and use.

Starting from your build-system, almost every tool and library we use in modern web development is not designed to work with workers. Luckily, our savior Vite comes to the rescue. It has built-in support for workers and makes it almost easy to use them.

To be fair, if you would not build/transpile your js, that would not be a problem. But thats currently not realistic in a bigger application.

So how do we create a worker with Vite?

A worker needs a url pointing to a js file. This part is taken care of by Vite. When importing a file we can tell Vite to import it as a worker with a ?worker parameter in the import path. This will return a function that we can use to create a new worker instance.

// main.ts
import MyWorker from "./worker.js?worker";
const worker = new MyWorker();

If we now log globalThis inside the worker, we can see that it is a different instance than the main thread.

// worker.ts
console.log(globalThis);
// > DedicatedWorkerGlobalScope {name: '', onmessage: null, onmessageerror: null, cancelAnimationFrame: ƒ, close: ƒ, …}

Now we would have to do the whole postMessage() and onmessage thing to communicate with the worker.

With Comlink we can use the worker as if we were calling a function in the main thread. We just need to wrap the worker we created with the Comlink proxy.

// main.ts
import * as Comlink from "comlink";

const worker = Comlink.wrap(new MyWorker());

And expose some functions in the worker.

// worker.ts
import * as Comlink from "comlink";

class MyWorker {
  static doSomething() {
    return "something";
  }
}

Comlink.expose(MyWorker);

Now we can call functions directly on the worker and it will return a promise.

// main.ts
const result = await worker.doSomething();

Easy.

TypeScript

Now this wont give us proper types for the worker, but we can fix that. First we need to export the class in the worker file.

// worker.ts
export class MyWorker {
  ...
}

Then we import only that type in the main file and type the comlink wrapper with it.

// main.ts
import MyWorker from "./worker.js?worker";

const worker = Comlink.wrap<typeof import("./worker.js").MyWorker>(new MyWorker());

const result = await worker.doSomething();
// result is now typed as string

Thats it :)