Canvas filled three ways: JS, WebAssembly and WebGL
Canvas filled three ways: JS, WebAssembly and WebGL

There are roughly speaking three ways to update an HTML5 canvas: plain old JavaScript, WebGL and WebAssembly. While JS and WebGL have their own ways of actually writing to the canvas, WebAssembly requires us to copy the resulting memory buffer to the canvas in JS. This might sound like a slow solution, but it still beats JS that gets bogged down even with a moderately complicated draw loop.

WebGL is really fast, but gets rather complicated if your effect has a lot of state that has to be passed and updated between frames, such as particles (it’s still possible by either handling the state in JS and passing it to WebGL, or even handling it purely in WebGL by storing it in a texture).

Let’s create a simple effect in all three ways and see how they work.

Javascript

Updating the canvas in JS is quite simple, even if we do it pixel by pixel. Here’s an example that colors every pixel red:

View in CodePen

Now let’s create our effect proper and see how it fares:

View in CodePen

Note that this could be optimized a lot. All the distance calculations are static between frames, we could easily precalculate them etc. But that is really not the point of this exercise, we just want something that’s heavy enough to show differences between the three methods.

JS can’t render the effect at 60 FPS even at 800×400. On my laptop I get around
45 FPS. Note that just updating the canvas with two prefilled arrays leaves the FPS short of 60.

WebAssembly

Let’s look at an MVP that colors every pixel red again:


View in CodePen

We have to pass a memory location to JS so that we know where to copy the data from. Other than that, the code is really similar to the JS version.

And now the full effect:


View in CodePen

Getting emcc to produce WASM with a sane amount of stuff imported from javascript etc. takes some time. The short of it is, use emcc -Os and check the WAT for required imports with something like wasm2wat.
You can take a look at the project for further details.

You’ll notice that we’re using a custom atan2 approximation: while emscripten happily produced WebAssembly with atan2 imported from C libs, the result was as slow as the JS version. We also use a custom FMOD, as emcc doesn’t seem to provide a native version of that. All floating point values are doubles: we don’t really need the accuracy, but it runs faster and creates a smaller WASM file.

We’re not doing dynamic memory allocation for the image data, as it doesn’t really affect the render speed and I can’t be bothered.

The JS code instantiates the WA module, sets up basic canvas stuff and copies the data over to the canvas in the render loop.

The performance is better than with plain JS, but I’m still getting only about 55 FPS with my laptop. For some reason there’s quite a bit of fluctuation in the FPS, at least in Firefox.

WebGL

This time our MVP has a lot more boilerplate:

View in CodePen

We have to create a rectangle to draw on, compile two shaders and update the time variable on each frame.

The boilerplate is the only drawback though, our full effect really doesn’t complicate the code much:

View in CodePen

The only out of ordinary thing here is that we request for high precision floats, as mobile devices produce garbled results if we don’t. WebGL is strongly typed and only likes to do some operations with floats, so pretty much everything here is floats. Many of the operations can be done on vectors, so the code is more compact and easier to read. WebGL’s Y axis is inverted, so as a lazy solution we flip the arguments to atan2.

WebGL is really fast. I get 60 FPS even on my phone.

Summa summarum

While JavaScript offers the best developer experience and the widest support, its performance leaves a lot to be desired.

WebAssembly is still somewhat painful to get running, but should get better over time. While it is a bit behind WebGL in performance, it does allow using existing C/C++ codebases which might come in handy.

WebGL’s performance is the best of the bunch, and would pull even more ahead if our effect was more complicated.
In some cases combining WebAssembly and WebGL might be a good idea.

I’d recommend going with WebGL if at all possible, writing it was simple enough once the boilerplate was done.

Timo Mikkolainen

Sulje