Run WebAssembly module in Web Worker with Parcel 2
Tags: parcel, webassembly, webworker
Published: 2021-02-21 09:11:05

I recently had to add JPEG compression in a web app bundled with Parcel 2. It wasn't easy to find a working example combining WASM + Web Worker + Parcel 2, so here is one ☝️

I am going to use the following packages  https://www.npmjs.com/package/@saschazar/wasm-image-loader and https://www.npmjs.com/package/@saschazar/wasm-mozjpeg.

Main thread
---

Parcel 2 supports Web Workers out of the box. It means that `new Worker('./mozjpeg-worker.js')` just works 👌 

_The rest of the code is essentially meant to wrap the lifecycle of the worker in a promise. `buffer` is obtained from `imageBlob.arrayBuffer()` and sent to the worker._

```js
// Main thread: mozjpeg.js

function processImageArrayBuffer(buffer) {
  return new Promise(function (resolve, reject) {
    const worker = new Worker('./mozjpeg-worker.js');
    worker.onmessage = function (event) {
      if (event.data.constructor === Uint8Array) {
        resolve(new Blob([event.data], { type: 'image/jpeg' }));
      } else {
        reject(new Error(event.data));
      }
      worker.terminate();
    };
    worker.postMessage(buffer);
  });
}
```

Web Worker
---

It's a bit more tricky within the Web Worker. I had to import separately the WASM glue code and the `.wasm` file.

I import the `.wasm` file with the prefix `'url:...'` so it can be fetched like a static asset. 

During the initialization of the module, I use the `locateFile` option to tell the glue code where is the `.wasm` file.

_The compression is done in two steps: decoding the array buffer (sent from the main thread) then encoding the result with compression parameters._

```js
// Web Worker: mozjpeg-worker.js

import wasm_image_loader from '@saschazar/wasm-image-loader';
import wasm_image_loader_binary from 'url:@saschazar/wasm-image-loader/wasm_image_loader.wasm';

import wasm_mozjpeg from '@saschazar/wasm-mozjpeg';
import wasm_mozjpeg_binary from 'url:@saschazar/wasm-mozjpeg/wasm_mozjpeg.wasm';
import wasm_mozjpeg_options from '@saschazar/wasm-mozjpeg/options';

const imageLoaderModule = new Promise(resolve => {
  wasm_image_loader({
    locateFile: function () {
      return wasm_image_loader_binary;
    },
    onRuntimeInitialized() {
      resolve(this);
    },
  });
});

const mozjpegModule = new Promise(resolve => {
  wasm_mozjpeg({
    locateFile: function () {
      return wasm_mozjpeg_binary;
    },
    onRuntimeInitialized() {
      resolve(this);
    },
  });
});

self.onmessage = async function ({ data }) {
  try {
    const { decode, dimensions, free: freeImageLoader } = await imageLoaderModule;
    const array = new Uint8Array(data);
    const decoded = decode(array, array.length, 3);
    const { channels, height, width } = dimensions();

    const { encode, free: freeMozjpeg } = await mozjpegModule;
    const result = encode(decoded, width, height, channels, { ...wasm_mozjpeg_options });
    self.postMessage(result.slice(0)); // Original array buffer is tied to web assembly module

    freeImageLoader();
    freeMozjpeg();
  } catch (error) {
    self.postMessage(error.message);
  }
};
```

That's it! For an exhaustive example, here is the Parcel configuration that I use 👇 

```json
// .parcelrc

{
  "extends": "@parcel/config-default"
}
```