Increasing Performance by Caching Paths on HTML5 Canvas

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.


Paget holmes
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.

Other considerations

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.

  • http://erisdiscord.github.com/ Eris

    I wrote a library a while back that let you do something like this with TexPlay in Ruby, but with syntactic sugar. I just ported that functionality to JavaScript (well, CoffeeScript) to see how it measures up and the results aren’t too bad.

    Here’s the CoffeeScript source for PaintCache and the performance results compared with bare drawing and sugar-free caching.

    As always, it’s a trade-off between beauty-and-convenience and performance: PaintCache fares pretty well against un-cached drawing, of course, but doing it the ugly way is still about twice as fast. Performance could probably be improved a little bit if the JavaScript output were hand-optimised (or rewritten by somebody better).

  • Pingback: Farkas Máté

  • John

    Hi Simon,

    Great article, and I’ll definitely test it out.

    If you do write a book (e-book?) about Canvas optimizations…  I’d buy a copy!

  • Saturnix

    Thanks! This was so useful! I’ll have to change an entire script, but I hope it will perform better. Will be following this blog :)

  • Ivanov Vitalii

    Hey Simon, perfect article! What about the book? It will be very helpfully.

  • http://www.simonsarris.com simonsarris

    I’ve actually been at work writing a book on HTML5 at large that will feature several Canvas tips, including performance tips. It will be taking up all my time, so a dedicated canvas performance guide will have to wait.

  • Richard

    Hi. how relevant/applicable is this with todays browsers? Still a big performance gain?

  • zoomclub

    This is very interesting, thanks. There is apparently an Path Object coming to the next version of the Canvas element. Wonder how the new Path Objects will compare in side by side performance tests to what you have discovered here?

    http://www.i-programmer.info/news/191-htmlcss/4015-html5-canvas-5-gets-new-features.html
    http://www.rgraph.net/blog/2012/october/new-html5-canvas-features.html
    http://kangax.github.io/jstests/canvas-v5

  • Naboo Khan

    Hi. A very scholarly article. I learned many information from the clear and confusing text. Now I understand and muddle the ideas, so I be a better programmer.

    Large thanks and condemnation

    Naboo

  • Kenyo

    Good and useful informations. I learn lots and teach myself this things. I know understand the way of the cache, which make me bigger in the browser.