Simple Web Workers workflow with webpack

What is Web Worker?

Web Worker is a simple way to separate scripts execution into background threads for web applications. A spawned worker can perform tasks and interact with main thread via messages API.

This is the basic diagram of Web Worker API. You can read more in following articles:

However, in most cases web apps require quite simple usage of web workers to fetch some big data or process a single expensive operation. This makes their usage quite complicated.

In this article I am going to describe how to use web workers in much more simple way.

Worker-loader

While webpack allows modules to be built with webworker target, in most cases it is excessive and not convenient as it requires additional configurations to be done.

More simple way is to use special worker-loader.

First install the loader plugin.

npm i -D worker-loader

Now, create your worker file.

// sample.worker.js
self.addEventListener('message', (event) => {
  if (event.data === 'ping') {
    self.postMessage('pong')
  }
})

The simplest way is to use inline loader query in your main script

// index.js
import Worker from 'worker-loader!./sample.worker';

let w = new Worker();
w.postMessage('ping');
w.onmessage = (event) => {
  console.log(event.data);
};

If you don’t want to use inline query, you can always define a rule in your webpack config

// webpack.config.js
{
  test: /\.worker\.js$/,
  use: [
    { loader: 'worker-loader' },
  ],
}

Now you can import the file directly and receive a worker constructor.

Promise workers

What if you need to separate a simple promise into its own thread? Writing a dedicated worker would produce excess code in most cases.

For such purpose we can use promise-worker package.

npm i promise-worker

It consists of two main components.

First allows to register a worker from a function. You should use it in your worker files.

Note that functions returning promises (or async functions) are supported as well

// sample.worker.js
import registerPromiseWorker from 'promise-worker/register';

registerPromiseWorker((message) => {
  return 'pong';
})

// OR

registerPromiseWorker(async (message) => {
  return 'pong';
})

Second allows to use created worker as a promise. You should use it in your main script.

// index.js

import PromiseWorker from 'promise-worker'
import worker from './sample.worker'

const promiseWorker = new PromiseWorker(worker());

(async () => {
  try {
    let response = await promiseWorker.postMessage('ping')
    console.log(response)
  } catch (e) {
    // handle error
  }
})();

// OR

promiseWorker.postMessage('ping').then(function (response) {
  console.log(response)
}).catch(function (error) {
  // handle error
});

Now you can execute the promise whenever you need it in your code.

Workerize / Workerize loader

Although the package looks very promising, I experienced several bugs and couldn’t make it run properly. When the article was written, it was on version 0.1.6. I hope developers will fix all issues soon 🙂

One more package, which deserves t is Workerize. First, install the package.

npm install workerize

The module exports a single function, which accepts a JS module as a string.

// index.js
import workerize from 'workerize'

let worker = workerize(`
  export const expensive = () => {
    let start = Date.now();
    while (Date.now()-start < 500);
    return a + b;
  }
`;

console.log('3 + 9 = ', await worker.add(3, 9));

Note that there is export directive in the code block. And also, exported functions must be pure — they must not depend on the main module.

In this case you don’t need any additional files and you can simply create threaded promises in your code.

In case you don’t want to use inline code, there are 2 ways. You. Can either use toString() method of a function:

function expensive = () => {};
let worker = workerize(`export ${expensive.toString()}`);

Or there is workerize-loader package, which is very similar to worker-loader, but it also allows to have multiple methods in the worker.

// sample.worker.js

export function one(input) {
  if (input === 'ping') return 'pong'
}

export function two(time) {
	if (input === 'foo') return 'bar'
}

The file above exports 2 different functions and you can use them in your main script.

import worker from 'workerize-loader!./sample.worker'

let instance = worker()
instance.one('foo').then(response => {
  console.log(response);
});

instance.two('foo').then(response => {
  console.log(response);
});

Greenlet

Greenlet is inspired by Workerize and also allows to create workers from pure functions. The syntax is very similar to previous package, however, you can pass a function directly as an argument.

npm i greenlet
// index.js

import greenlet from 'greenlet'

let promise = greenlet(async url => {
	let res = await fetch(url)
	return await res.json()
})

console.log(await get('/foo'))

The disadvantage here is that you CANNOT use any custom modules or imports inside the function. The package is also very new and it will probably be improved with time.


All Rights Reserved