On the fly WebP decoding using WASM and a Service Worker

For the people who know me, I have been super excited by the recent developments on the web platform, such as modules, service workers and now even Web Assembly!

It is amazing seeing the momentum and the progress being made, with the W3C working group, which I am a member of, just having release its first working draft of the standard:

A little while back, back in November, I did a presentation of Web Assembly at the yearly W3C TPAC meeting — this year in the bay area:

TPAC WebAssembly presentation

One of the quick demos that I showed, was decoding "webp" images in both Chrome and Firefox. libwebp already had some experimental WASM support based on SDL, which Emscripten has support for via the canvas element.

That worked fine, but wouldn't it be nicer if people could just import a library and then things would just work out of the box? I thought so, because as you see above, using webp required some invasive code changes like manually calling JavaScript functions and thus doesn't work everywhere like in CSS.

Service Workers allows a site to intercept fetches of resources, and as it happens to be, you can use WebAssembly in a Service Worker too! I was super excited about spending some more time on this, but being busy with lots of things, I only had time to look at this now.

What prompted me to actually spend some time on this was Surma's great tweets and later his article about how to write a WebP decoder using WebAssembly — I highly recommend you to check that out.


I am happy to say that you can indeed make a Service Worker decode WebP images on-the-fly already with some quirks that doesn't make it deployable yet :-) — or at least I haven't found a way, yet.

[March 10: Update: It can be deployed now without any experimental features, check the update section at the end of this article]

First of all, in order to get a blob that the browser can render as an image (ie. image/png blob) you need to use a canvas. Canvas is not available in workers, and thus service workers, unless you use the Offscreen Canvas, which is only implemented behind flags in Chrome and Firefox — and only works reliable in the former. The API between the two also differs.

Decoding WebP images in a Service Worker on Chrome, using Offscreen Canvas

Another issue I ran into, is that you cannot use the same WebAssembly library in both the Service Worker and the main context, at least at this point. In the screenshot above, I decoded 3 images in the Service Worker and the 4th in the main context and as you can see from the DevTools output, the latter fails.

If I turn off the service worker (Bypass for network), the first 3 images are rendered directly by Chrome, which has native support, and the 4th by the WebAssembly library:

4th image decoded in main context by WebAssembly in Chrome

This also works fine in Firefox, which will only show one image, as it has no native support for WebP images:

4th image decoded in main context by WebAssembly in Firefox

Creating the library

The libwebp library is a really simple C API:

This makes it super easy to get started. All I need to do is expose two functions, decode() and getInfo(). The latter is needed in order to get the size of the image so that I can allocate the right sizes buffers etc. Unlike Surma, I am not creating my own allocate and deallocate methods because Emscripten provides those for me in the form of Module._malloc() and Module._free() . The libwebp docs will tell me to free via WebPFree() but that method just calls the regular C version of free so using Module._free() is fine!

My mini WebP decoding library to be compiled using Emscripten to WASM

Setting up an automated build system using CMake was a bit more of a challenge :-) but I managed with a bit of help from good friends:

A simple CMakeLists.txt file building my webp decoding library

In order to make it easy to compile, I added the build commands inside my package.json so that all I need to do is call npm run build:

Finding the right command needed to build the wasm module was not straight forward

Approach and complications

Now we have the library we need to wrap it into something nicer, so that the user doesn't need to know about buffers, allocations, de-allocations, and the special inner working of Emscripten.

I created a mini JS library called webp-decoder.js that should work from the main context and from a worker. It exposes an async function called fetchWebPDecoder returning a WebPDecoder object with a few methods.

Emscripten will create a global Module object, or use yours if you supply it. As we want to know when everything has been loaded, we need to supply our own. Though something like var Module = {} works fine directly in the main context or directly in a service worker, it won't work in libraries imported from these places, so the below fixes that — and is very similar to what Emscripten does itself.

We now create a Promise that will resolve when everything is properly loaded, which is nice because we can just block on it ("await" it) before we do further processing. In order for the onRuntimeInitialized to ever be called, and your promise resolve, we need to load the JS boilerplate code generated by Emscripten. In a worker that means calling the sync importScripts as Modules don't work in workers yet, even though you can configure a worker as being an ES module using { type: "module" } eg:

We want our Service Worker to be an ES module, but this isn't working yet :-(

The final code looks like this:

I implemented a simple importClassicScript for the main context, and that works fine.

What unfortunately doesn't work fine, is calling importScripts from within a JavaScript file loaded via importScripts so that will have to be done manually by the user. ES module should solve all of this, so I am really looking forward to that working in workers!

To get access to the C functions we exposed, we use Emscripten's cwrap:

But as these are quite low level, I wrap it all up in a WebPDecoder class with a few methods. Let's look at these!

The constructor takes an ArrayBuffer, for which we create a typed array, an Uint8Array as libwebp excepts an const uint8_t* pointer. We need to copy the data to the WebAssembly heap, so first we allocate a buffer and then we copy the data. For allocation, we allocate byteLength bytes using _malloc and then we copy the data using Module.HEAPU8.set() — all convenience APIs exposed by Emscripten.

Exposing version() is easy as it just returns a number, so we just call the function we got back from cwrap. The info() method is a bit more complicated. In C, we return an integer array (pointer) of the size 3, where the first value is whether the method succeeded or not, and the next two, the width and height of the image, respectively.

On the JavaScript side, we use getInfo from Emscripten to get the values at their respective offset and then we free the memory afterwards, as getInfo allocated internally.

The C decode method also allocated internally, and it returns a buffer of uint8_t values, which we need to copy to the JavaScript side and expose as an Uint8ClampedArray. We need the "clamped" array because the ImageData objects can only work with that, and the data is already clamped :-)

Let's look at the resulting code:

We decode, then we create a "view" on top of the decoded data. This is then copied as we create the Uint8ClampedArray and we can therefore free the decoded data.

An Uint8ClampedArray is not that useful for actually displaying the image on the screen, as what we really want is a Blob of format image/png. As this is a bit of work, we create a convenience method, which takes a canvas (offscreen or not, so it works on main context and in a worker):

We basically need to create an ImageData object of the raw data and then paint that to a canvas. After that we need to check whether it is an OffscreenCanvas or not and here you notice that the experimental API differs between Firefox and Chrome.

If it is not an offscreen canvas, we need to just call toBlob() but as that API is designed before the times of Promises, we need to wrap it.

Yay! We now have a nice library for decoding WebP images!

How to use

Let's start with the main context. We just manually fetch a file, get it's buffer and then decode it. The result it then added to a new img element.

As the WebAssembly code and thus my WebPDecoder is loaded async, async/await really saves the day here:

Interestingly enough, the code looks quite similar in the Service Worker:

Here we intercept all GET calls to resources ending in ".webp", fetch the resources, get the array buffer, and the WebPDecoder, decode using an offscreen canvas, and then return a Response based on the blob data. Quite nice.

As I mentioned earlier, we cannot call importScripts from inside a JS file loaded via importScripts so I create a fetchWebPDecoderWithWorkarounds() function, that does that manually as well as blocks until the WASM module is fully loaded:

In order to be able to actually block, I need to create the promise on the global stack.

The offscreen canvas has to be created on the main context and sent to the service worker:

On in the Service Worker, we have to block on it being available as well, with a similar trick:

We all that set in place, you can now decoded webp images in both Firefox and Chrome, and in Chrome using a Service Worker! I am pretty excited about the future implications of this, especially when we got ES module support in workers!

Creating canvasses and sending them around is a bit complicated so I wish there were some easier APIs for that, as the actual canvas might not be required for this use-cases. I saw that there has been a few proposals for that before, like:

That is it for now, folks! Check out the code here:

A reminder

Also, don't forget to turn on the required offline canvas flags in Chrome and Firefox. Depending on your Chrome version, you might have to turn on "Experimental Web Platform Features" as well:

about:flags in Chrome

An update

Domenic Denicola pointed out to me on Twitter, that the web also supports BMP images, which is great because BMP images do not compress their graphical data. Wikipedia also has a great explanation on how the format works, so it was quite straight forward building an encoder:

The resulting encoder is around a mere 60 lines. Very nice!

With that the Service Worker code gets simplified to:

The cool thing is that this now all works in Firefox, as can be seen below!

Firefox can now render WebP images via a Service Worker and with the help of Web Assembly!