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);
});
}
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);
}
};
That's it! For an exhaustive example, here is the Parcel configuration that I use 👇
// .parcelrc
{
"extends": "@parcel/config-default"
}
Top comments (0)