08 May 2020, 18:28

Writing Web Workers in TypeScript

TypeScript has taken the web development world by storm, and I too am a fan. Unfortuantely what I’m not a fan of is contention on the main thread, which has increased over time as we ship more and more JavaScript to our pages.

I’ve written in previous posts about Web Workers, but for those of you note familiar they allow the developer to move work off of the main thread and into a separate thread of execution. These work great for tasks that often block such as data crunching in audio, gaming and mapping applications. We can also leverage them for more generic work, and Surma has done a great job of explaining why that is an important consideration for web developers.

In this post, I want to show how you can write Workers in TypeScript and build them using the popular bundler Webpack. The first step we need to take is to install all the modules we need via npm as development dependencies. We can do this from our command line like so:


We also need to set up a TypeScript configuration, `tsconfig.json`, file in our root directory.  We can do a rudimentary implementation like this:

```javascript
{
    "compilerOptions": {
      "outDir": "./dist/",
      "noImplicitAny": true,
      "module": "es6",
      "target": "es5",
      "allowJs": true,
      "sourceMap": true
    }
}

You can adjust this to your required tastes but this is a barebones starter to get going. Next lets setup the webpack.config.js file again in our root directory to configure Webpack and allow us to build our application and worker:

const path = require('path');

module.exports = {
    mode: 'development',
    entry: './src/index.ts',
    devtool: 'inline-source-map',
    module: {
        rules: [
            // Handle TypeScript
            {
                test: /\.tsx?$/,
                use: 'ts-loader',
                exclude: [/node_modules/]
            },
            // Handle our workers
            {
                test: /\.worker\.js$/,
                use: { loader: 'worker-loader' }
            }
        ]
    },
    resolve: {
        extensions: ['.ts', '.js']
    },
    output: {
        // This is required so workers are known where to be loaded from
        publicPath: '/dist/',
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'dist/')
    }
};

This covers the build step side of things, now we can look at our code itself. Let’s assume we have a src folder for our source code, and a dist folder for a compiled code. The first thing we’ll want to do is setup types for the Workers so that TypeScript doesn’t complain:

// types.d.ts
declare module "worker-loader!*" {
    class WebpackWorker extends Worker {
      constructor();
    }
  
    export default WebpackWorker;
}

Now, let’s write a Worker. As an example of a large workload, this Worker will generate primes using the Sieve of Erastosthenes and return them back to the main thread:

// worker.js

// We alias self to ctx and give it our newly created type
const ctx: Worker = self as any;

class SieveOfEratosthenes {
    
    // This is the logic for giving us back the primes up to a given number
    calculate(limit: number) {

      const sieve = [];
      const primes: number[] = [];
      let k;
      let l;

      sieve[1] = false;
      for (k = 2; k <= limit; k += 1) {
        sieve[k] = true;
      }

      for (k = 2; k * k <= limit; k += 1) {
        if (sieve[k] !== true) {
          continue;
        }
        for (l = k * k; l <= limit; l += k) {
          sieve[l] = false;
        }
      }

      sieve.forEach(function (value, key) {
        if (value) {
          this.push(key);
        }
      }, primes);

      return primes;

    }

}

// Setup a new prime sieve once on instancation 
const sieve = new SieveOfEratosthenes();

// We send a message back to the main thread
ctx.addEventListener("message", (event) => {

    // Get the limit from the event data
    const limit = event.data.limit;

    // Calculate the primes 
    const primes = sieve.calculate(limit);

    // Send the primes back to the main thread
    ctx.postMessage({ primes });
});

And then back to our main thread we instantiate the worker and send a message asking the first 1000 primes:

// index.ts

// Not the worker-loader! syntax to keep Webpack happy
import PrimeWorker from "worker-loader!./worker";

const worker = new PrimeWorker();

worker.postMessage({ limit: 1000 });
worker.onmessage = (event) => {
    document.getElementById("primes").innerHTML = event.data.primes;
};

Now if we can build the file. Here we assign a build script "build": "webpack" in our package.json which will build the file for us into a dist directory as bundle.js. This can then be referenced inside your webpage of choice.

If you want to see the full working example I’ve posted it to this GitHub repository for you to experiment with.