I’m writing a book on HTML5, including Canvas! Click here for more information.
Much of the functionality of Canvas comes from its path drawing functions. Unfortunately for game designers and animators, re-drawing paths over and over can amount to a tangible performance hit. To increase performance, let’s take a look at caching paths as images to avoid redrawing them traditionally.
Gentlemen waiting for a path to finish rendering
First we need to ensure that caching a path will actually lead to a performance increase. We can devise a simple test for this using JSPerf. We need a path to test, so let’s write something fairly simple.
ctx.beginPath(); ctx.strokeStyle = 'red'; ctx.lineWidth = 4; ctx.moveTo(10,10); ctx.lineTo(10,30); ctx.lineTo(30,30); ctx.lineTo(40,70); ctx.quadraticCurveTo(72,43,22,12); ctx.quadraticCurveTo(12,43,12,102); ctx.stroke();
This does not produce a particularly complex or exciting path:
But it will do.
The thing we want to ponder here is whether redrawing this path, in other words executing every one of the instructions needed to make the path, will be a slower process than if we cached it. We can achieve caching by drawing the path from these instructions only once, to an in-memory canvas, and then using drawImage from our in-memory canvas onto our real canvas to redraw the path.
That isn’t the only way to cache. We could instead draw it to a canvas and then make a PNG out of it, and call drawImage from that PNG instead, but for the sake of making a simpler test we will stick with using an in-memory canvas.
So let us take all of the drawing instructions above and execute them on the in-memory canvas. Then in our draw loop, instead of drawing out the path every time, we simply draw the in-memory canvas to our real canvas:
// can2 is our in-memory canvas ctx.drawImage(can2, 0, 0);
The test is simple enough. Giving it a go, the results are immediately clear: pre-rendering and using drawImage is more than ten times as fast as drawing the path, even for the relatively simple path used!
The more complex the path, the more time you will save with caching. If you’re using a lot of complex paths to render shapes, such an optimization ought to speed up your draw loop by a great deal. The JSPerf page shows a simple setup if you want to make the test for yourself.
Pre-rendering paths isn’t a magic bullet, there are still a lot of uses for drawing paths constantly in canvas. If you are making a live drawing application, or otherwise constructing dynamic paths and/or moving and animating paths on the fly, then any kind of pre-rendering is going to be nearly pointless or even harmful to performance. After all, what’s the use of drawing something to a separate canvas and drawing that state back to the original canvas if the path changes constantly? You’d need to re-draw the in-memory canvas just as often, so you’d lose all benefit.
It is also worth mentioning that you might want to play around with using PNGs instead of in-memory canvases. Another thing to test is putting multiple paths onto one in-memory canvas versus putting them all in their own separate canvases, effectively making a sprite sheet. From previous tests, it seems that there is a slight advantage to giving each sprite its own png instead of using a single-png (or single in-memory canvas) sprite-sheet, but it wasn’t that big of a difference.
If you do choose to use a sprite sheet, note that there are a lot of decent tools out there for compressing and organizing them, such as Sprite Sheet Packer.