Optimising HTML5 Canvas games
In this blog post I’m going to share a few tips and tricks I’ve picked up from the process of refactoring HTML5 Breakout. Some of these will be common sense performance tips; others might leave you scratching your head a bit. I’ll start with a few general tips, and then move on to the canvas specific ones. (The best stuff is at the bottom).
Don’t prematurely optimise your code
This can cause all sorts of bugs, like breaking your collision detection. You’ll end up having to backtrack, and it can become a bit of a nightmare, so it’s best not to do it!
Profile your code
Once you’ve finished your game, you need to profile your code. It’s important to benchmark your code on all of the browsers you’re targeting. Modern browsers compile and optimise your code, but the different JS engines do it in slightly different ways, so the function that runs lightning fast in one browser may perform sluggishly on another (for example, see ‘clear methods’ below). David Mandelin at Mozilla did a great presentation on JS engine internals at VelocityConf, it’s well worth going through the slide deck if you’re interested in the nitty gritty. Go for the low hanging fruit here and investigate any code that is reported as running slow across all your profiles.
Minimise code in loops
This is a common sense best practice, but it becomes especially important when you’re doing something as CPU intensive as repeatedly drawing images to canvas. When I was profiling Breakout, I discovered one function that was taking a particularly long time to execute. It turned out that I had a call to fillRect in one of the loops in the drawBricks function. Simply moving this one line out of the loop shaved 50% of the execution time of that function. Little fixes that result in huge performance increases are always the most satisfying
Minimise draw calls
As mentioned above, constantly drawing and redrawing images is hard work, so don’t draw if you don’t have to! It can be tempting to draw a frame and then clear the whole canvas repeatedly in your game loop, as it simplifies development. However, it doesn’t do much good for your game’s performance. If you can figure out which parts of your canvas have changed since the last frame, and only redraw those parts (by using ‘dirty rectangles‘), your game should see a huge performance boost.
Use more than one canvas
If you draw too many pixels to the same canvas at the same time, your frame rate will fall through the floor. In these circumstances, it’s better to use multiple canvasses layered on top of one another. Take Breakout for example. Trying to draw the bricks, the ball, the paddle, any power-ups or weapons, and then each star in the background – this simply won’t work, it takes too long to execute each of these instructions in turn. By splitting the starfield and the rest of the game onto separate canvasses, I was able to ensure a decent framerate. This is a useful technique for animation-heavy games.
If you’re a front-end developer used to working with the DOM, the concept of sub-pixel rendering will be a little odd – we’re just not used to seeing anything with units of measurement less than a pixel. Canvas can render your images at positions less than a pixel, anti-aliasing them in the process. Anti-aliasing canvas is currently super slow on some platforms, so it’s best to avoid it if you can by rounding off the positions of your game entities just before each frame is drawn. I’ve been using | 0 (bitwise OR) – it’s faster than Math.round() as it doesn’t have the function call overhead, but it seems like the best method changes with each incremental browser version. Check with JSPerf and pick the best method for your use case. And read Seb Lee Delisle’s HTML5 canvas sprite optimisation post, he explains this much better than I do
There are three ways to clear your canvas: fillRect using your background colour, clearRect, and resetting the canvas’ width (or height) property. Resetting the width of the canvas is supposedly the fastest method, but if you look at the JSPerf tests – particularly the differences between Chrome 14 and Firefox 4 – the execution speeds documented are wildly at odds with each other. From this data, it appears that width = width performs at the same speed or faster (much faster in some instances) than the other two methods on mobile, but when it comes to desktop browsers, all bets are off.
Another issue worth thinking about – if you’re doing a lot of transforms, width = width resets the transform stack, whereas clearRect and fillRect don’t. So if you’re doing a lots of transforms and don’t want to reset state, clearRect is probably the best way to go. Simon Sarris did a good write up of this called How you clear your HTML5 Canvas matters.
|UserAgent||clearing non transformed canvas||clearing transformed canvas||setting width||# Tests|
Use requestAnimationFrame instead of setInterval/setTimeout
SetInterval and setTimeout were never intended to be used as animation timers, they’re just generic methods for calling functions after a time delay. If you set an interval for 20ms in the future, but your queue of functions takes longer than that to execute, your timer won’t fire until after these functions have completed. That could be a while, which isn’t ideal where animation is concerned. RequestAnimationFrame is a method invented by Robert O’Callahan at Mozilla – it specifically tells the browser that an animation is taking place, so it can optimise repaints accordingly. It also throttles the animation for inactive tabs, so it won’t sap your mobile device’s battery if you leave it open in the background.
Nicholas Zakas wrote a hugely detailed and informative article about requestAnimationFrame on his blog which is well worth reading. If you want some hard and fast implementation instructions, then Paul Irish has written a requestAnimationFrame shim – this is what I’m currently using in my games.
Canvas has a back-reference
And finally, a useful tip I picked up at Remy Sharp’s HTML5 Workshop – your 2d context has a back reference to it’s associated DOM element:
var ctx = doc.getElementById('canvas').getContext('2d'); console.log(ctx.canvas); // HTMLCanvasElement
This can come in pretty handy!
I hope you’ve enjoyed my whistle-stop tour of canvas performance optimisation techniques. If you spot any errors or glaring omissions, please leave a comment below