DEV Community

Gilles
Gilles

Posted on

Run WebAssembly module in Web Worker with Parcel 2

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.

// 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);
  });
}
Enter fullscreen mode Exit fullscreen mode

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.

// 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);
  }
};
Enter fullscreen mode Exit fullscreen mode

That's it! For an exhaustive example, here is the Parcel configuration that I use πŸ‘‡

// .parcelrc

{
  "extends": "@parcel/config-default"
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)