loading...
Cover image for Seamless web workers & worker threads - threads.js

Seamless web workers & worker threads - threads.js

andywer profile image Andy Wermke ・4 min read

A quick leap into running cross-platform Javascript / TypeScript code in workers. Using a transparent API – minus the hassle.

After half a year in beta, threads.js v1.0 has finally been released. It allows you to use web workers and worker threads in an intuitive way, provides features like thread pools, works in web clients as well as node.js and is less than 10kB in size!

The benefits of using workers have already been covered in plenty of other articles, so here is the gist:

  • Leverage all the available CPU power to run heavy CPU-bound tasks
  • Move all non-rendering code off the main thread to ensure smooth animations and a responsive user interface (web workers)
  • Isolate software modules, restrict them to communicate via message passing

Now even though the web worker and worker threads APIs are similar, unfortunately they are not fully compatible. Furthermore, they are rather low-level building blocks: Create a worker, subscribe to message, post a message, etc.

What happens if you post some messages to a worker and one causes the worker to throw? Chances are the calling code will never know there was an error - it will just not receive a response message. And then there is all the glue code…

Powers of a transparent API

Enter the stage… threads.js! Let's write a simple worker that hashes passwords for us.

// workers/auth.js
import sha256 from "js-sha256"
import { expose } from "threads/worker"

expose({
  hashPassword(password, salt) {
    return sha256(password + salt)
  }
})

Now let's write the main thread's code – spawn a new worker and hash a password.

// master.js
import { spawn, Thread, Worker } from "threads"

async function main() {
  const auth = await spawn(new Worker("./workers/auth"))
  const hashed = await auth.hashPassword("Super secret password", "1234")

  console.log("Hashed password:", hashed)

  await Thread.terminate(auth)
}

main().catch(console.error)

It's simple. Expose a function in the worker, call it from the other thread – done!

Note that auth.hashPassword() will always return a promise, whether hashPassword originally returns a promise or not – the return value will be promisified, due to the async nature of worker communication.

So what about error handling? It's simple as we are now working with a promise-based API.

// master.js
import { spawn, Thread, Worker } from "threads"

async function main() {
  let auth, hashed

  try {
    auth = await spawn(new Worker("./workers/auth"))
  } catch (error) {
    // Cannot spawn the worker or crashed immediately before doing anything
  }

  try {
    hashed = await auth.hashPassword("Super secret password", "1234")
  } catch (error) {
    // Hashing this password failed
  }

  console.log("Hashed password:", hashed)

  await Thread.terminate(auth)
}

main().catch(console.error)

By the way, did you notice Thread.terminate()? We use it to terminate a worker once we are done using it.

Run in node.js

Let's take our previous sample code and change the ES module import statements into require() calls for now. You can clone the code from this GitHub Gist.

$ git clone git@gist.github.com:925395687f42f6da04d111adf7d428ac.git ./threads-gist
$ cd threads-gist
$ npm install

Running it is trivial.

$ node ./master

This code will run in any node.js version that comes with worker threads support, so node 12+ or node 10+ with a feature flag set.

You can even run it on node 8.12+. Install the tiny-worker npm package – threads.js will automatically pick it up as a polyfill if worker threads are not available.

Build using webpack

A vast number of people use webpack to bundle their code for front-end deployment. So how do we now make that code build with webpack?

We use our code as is, take our webpack config and add the threads-plugin – that's it!

  // webpack.config.js
  const path = require("path")
+ const ThreadsPlugin = require("threads-plugin")

  module.exports = {
    entry: {
      app: "./src/index.js"
    },
    mode: "development",
    module: {
      rules: [
        {
          test: /\.css$/,
          use: ["style-loader", "css-loader"]
        },
        {
          test: /\.jsx?$/,
          use: ["babel-loader"]
        }
      ]
    },
    output: {
      path: path.join(__dirname, "dist")
    },
    plugins: [
      new HtmlPlugin(),
+     new ThreadsPlugin()
    ]
  }

The plugin is based on Google's worker-plugin – it will recognize new Worker() expressions, make sure the referenced worker file is bundled independently from the main entrypoint and rewrite the path in the new Worker() expression to the worker bundle path.

First class TypeScript support

Threads.js is written in TypeScript and thus fully statically typed, so your IDE's IntelliSense will show you all available exports, functions and parameters with documentation as you write. That's not all – even running TypeScript workers becomes easier.

When running your TypeScript code in node.js you will frequently find yourself using ts-node in development and running the transpiled JavaScript code in production.

When resolving a worker, threads.js will try to load the transpiled JavaScript worker. If that fails and ts-node is installed it will automatically fail-over to run the untranspiled TypeScript worker file using ts-node. You don't have to lift a finger 🙌

Thread pools, observables & more

There is more to explore!

  • Thread pools to spawn multiple workers and dispatch jobs to them
  • Returning observables to expose events to subscribe to
  • Support for transferable objects to efficiently pass binary data
  • and more… 🚀

You can find the detailed documentation on threads.js.org.

There are also more features to come. Check out the GitHub repository and its issues to see what's in discussion right now or watch the repository's releases to stay up to date.


That's it for today – I hope you enjoyed the blog post. If you like the project, give the repository a 🌟 on GitHub, contribute to the development or become a sponsor.

Feel invited to comment and leave feedback of any kind below.

Happy holidays and happy hacking!

Andy

Teaser image by Adi Goldstein on Unsplash.

Posted on by:

andywer profile

Andy Wermke

@andywer

Software engineer & creator of internet things. Node.js aficionado since 2011, React lover since 2014. Head of solarwallet.io at SatoshiPay (satoshipay.io).

Discussion

markdown guide
 

Looks a lot like Comlink and workway which are much lighter... (arround 1kb each).

 

I've never understood the JS community's obsession with smallest libraries. 1kB vs 10kB makes exactly no difference for download nowadays. Basic HTTPS handshake takes more time than downloading that amount of data.

 

Well, JS costs more than just the kb to download. And I disagree with your statement 10kb instead of 1kb, when you're on a low 3G or even on Edge, can make a huge difference !

Still, thread.js is interesting, being able to pass observable thru worker is great !

 

They play in the same ball park, but Comlink only works with web workers (no node.js support) and lacks a bunch of features, like out-of-the-box support for TypeScript workers without transpilation, support for observables and some more things.

Not sure about workway. It seems to support web workers and node worker threads, but won’t work easily with webpack and seems to lack the TypeScript features, for instance.

 

Do I need web workers and worker threads. For basic web design/development?

 

If you don’t know if you need them chances are you don’t 😉

Doing a lot of network communication or parsing a lot of JSON data can already impact performance. Once you see performance issues, laggy animations, etc. you can look into it.

 

What is the channel of communication between the UI thread and workers? Just a json string?

 

Hi! It uses worker.postMessage() to post an unserialized object, since this is the fastest option. The browser / node.js can then determine what’s the most efficient way to pass the data.

For a worker invocation myWorker.print({ foo: "bar" }) the posted data would look roughly like this: { callID: 27, method: "print", args: [{ foo: "bar" }] }