Note : this post is from 2011, the techniques described here are probably no longer necessary
If we’re serious about making HTML games then we need to know the most efficient ways to render multiple sprites. Many are saying that Canvas isn’t fast enough for gaming and we should use DOM objects instead. But before we give up Canvas altogether, let’s see if we can squeeze out just a little more performance…
A little while ago my podcasting mate Iain Lobb converted his sprite blitting test “Bunny Benchmark” to HTML5 canvas and was impressed with the results – he got 3000 bunnies rendering quite comfortably on all of his browsers.
But I was massively unimpressed – I could only get 5-10fps on all of my browsers. The reason for the difference in performance – Iain’s on Windows and I’m on OSX.
I did some hunting and I found this benchmark on JSPerf which showed that if you call drawImage on the Canvas element, it’s much faster if you round the x and y position to a whole number.
Whole pixel bunny vs sub-pixel bunny
If you draw into canvas using sub-pixels (not whole numbers) the browser will interpolate the image as though it was actually between those pixels. It’ll give you much smoother animation (you can genuinely move at half a pixel per update) but it’ll make your images fuzzy.
And it seems that browsers in OSX are super slow at doing this interpolation.
The solution : round your x and y position to whole numbers before rendering.
[GEEKY DIVERSION]
A quick search on JSPerf shows that using a sneaky bitwise operation is faster than the built in Math.round.
If you apply a binary NOT (represented by the tilda : ‘~’) every bit that was at 1 is now 0 and vice versa. If you do that again, the number is switched back to what it was. Except that any bitwise operation takes away any digits after the decimal point, which rounds it down to the nearest whole number.
But we’re trying to round to the nearest whole number, not just round down (like Math.floor). So if we add 0.5 first it rounds up to the next number if we’re close to it, so the final operation is:
x = ~~ (x+0.5);
Note that this only works on positive numbers!
[UPDATE] thanks to lab9 in the comments below who reminded me that you can do a binary OR with 0. This compares every binary digit in your number to every digit in 0 :
00110110010 OR
00000000000 =
00110110010
In other words, it does nothing! But again, because it’s a binary operation, you lose everything after the decimal point. I just added it to the JSPerf test and it’s even faster – thanks again lab9!
Now I wouldn’t normally advise premature optimisation, especially if it makes your code unreadable, but as we’re making a benchmark I thought it would probably be fair.
[GEEKY DIVERSION ENDS]
And so without further ado, see the results for yourself:
Bunny Benchmark with snapping option
I get a massive improvement on most browsers – 7fps to 30fps. It’s only Firefox that only improves marginally. 7fps to 10fps, even on FF4. Is it the same for you? Is there a improvement on Windows with snapping? Do any of you know how I could implement something like this on JSPerf.com? I’d like to hear from you!
Coming next: DOM object bunnies!
We’ll be covering this and more at my Creative JS workshops – next one in Brighton, only one space left!
38 replies on “HTML5 canvas sprite optimisation”
I think you could also use a single bitwise operation :
x = (x + .5) | 0;
never tested it in js, but works great in AS.
Yes! I knew there must be a quicker way to do this… I just added the binary OR version in JSPerf and it’s even faster – thanks Lab9 π //jsperf.com/math-round-vs-hack/3
π
In as3, it was even faster than casting to an int when I tested it.
Cheers Seb, this should help one of my current canvas tests run faster π
Chrome 9 on Windows I get a very subtle ~57fps without snapping and about 60 with.
I’m on MacBook Pro 2.8Ghz C2D 4Go 10.6.6 and I get:
FireFox 4b10: 8fps vs. 15fps
Chrome 8: 9fps vs. 30fps
Opera 11: 8fps vs. 25fps
Safari 5: 12fps vs. 30fps
Windows 7 – 2.8 Ghz – dual core
Firefox 3.6: 2 to 3 fps. With snapping 2 to 8 fps
Chrome 8: 57 fps (I don’t see any change with snapping)
Safari 4: 12 fps. With snapping 16 to 29 fps
The slow ones can really drop the frame rate with simply moving the mouse around.
actually I did the jsperf test on firefox 3.6 and.. the fastest method is Math.round! and the difference is huge. Probably because the engine is trying to do some optimizations itself.
so, no easy choices here.
You make an excellent point here wildcard. The difference in performance is imperceptible compared to the improved rendering performance. The lesson : always concentrate on the rendering bottleneck first!
I’m loving how Iains bunnies keep appearing everywhere…
Btw, I seem to remember flash used to need hacking to make it run just as fast on a mac. About 5-10 years ago.
You’d think Mr Jobs would give this issue his attention…
Actually, it will only work with positive signed 32-bit floats, i.e. numbers from 0 to +2,147,483,647 (
2^31-1
).For some more binary number-rounding hacks and their performance, see //jsperf.com/rounding-numbers-down.
Nice one Mathias, you’ve officially out-geeked us all π thank you!
I have updated the number of test cases : //jsperf.com/math-round-vs-hack/4
and I was amazed to see how wildly different the results are depending on the browser. I would suggest using the method which seems the most consistent across browsers, which SEEMS to be the double-not hack.
Note:
All tested on a macbook pro using arora, camino (which doesn’t seem to show up on the results table), chrome, firefox, opera and safari
Thanks for sharing, Seb. Sadly canvas performance in mobile Safari on iPhone is still awful, around 1fps. Hm…
I’ve found that too – canvas is just crap on iOS! Let’s see how the DOM bunnies work though… π
interesting points, just to add another finding:
IE9 platform preview on Win7 64 ~45-51fps almost equal in both settings. Seems only FF improves a bit (3.6.13) but still way below IE9 ~50 and Chromes ~60 fps…
I’ve added a couple more stuff to the JSPerf: //jsperf.com/math-round-vs-hack/5 – check the notes at the beginning as well.
I’ve wrote a couple months ago about premature/micro optimization (//blog.millermedeiros.com/2010/10/the-performance-dogma/) and in this case I believe that rounding shouldn’t be the bottleneck (as you also said) so it shouldn’t be optimized unless you really need it and know the target environment (since results vary so much).
It is really good to know that avoiding sub-pixel rendering can improve performance that much. Thanks for sharing.
[…] Lee-Delisle shows a great speed improvement to Canvas sprite animations by avoiding sub-pixel […]
That’s super fast – very nice… im curious to see if you made the canvas element the full browser size how it would perform.
This brings up another point of optimization that is a double edged sword.
Clearing the entire canvas, is usually slower than clearing only rect only the ‘dirty’ areas, but at this high volume, it would probably be much slower.
i think ill try making a test to confirm
Forgot to say, I’ve posted some other stuff on Quora about how to improve canvas performance as well: //www.quora.com/What-is-the-best-way-to-get-faster-frame-rates-with-HTML5-Canvas/answer/Miller-Medeiros
Well, my FF got 1 fps unchecked and 5 checked, so improvement was massive.
However, when I switched to chrome (which finally showed me what I was supposed to see) – 30 fps either way
Using Chrome (on Windows 7) I see no FPS difference with snapping on or off – it always stays above 50 – usually at about 56 or 57 regardless.
[…] can now see the most popular jsperf test cases HTML5 canvas sprite optimisation shows you why you should round your numbers to the nearest whole number when working with canvas. […]
I’ve never seen sub-pixel on windows (where I do all my canvas coding).
So OSX is slower because it does something extra.
But sub sampling could be useful sometimes, so i’d like an option to switch it on-off directly form the canvas API.
Hi Omiod,
I’m pretty sure you get sub-pixel on Windows but I’d love for you to check! Just render a single bunny at a position that changes 0.05 every frame. If the bunny appears to move smoothly, you got sub-pixel. If he snaps from pixel to pixel, you don’t. I’d love confirmation on this one way or another!
Seb
[…] on x/y coordinates of the ball (for pixel snapping purposes – read Seb Lee-Delisle’s Sprite Optimisation post for more details) caused a few problems with collision detection later on. Β I’ve […]
[…] Β 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 […]
The JSPerf stuff is really useful.
One way I got good performance for my landscape ( //rathouse.net/landscape/ ) was to use getPixelData / putPixelData and directly write the RGBA values.
I found myself nodding my noggin all the way thorugh.
[…] HTML5 canvas ΠΏΠΎΠ΄Π΄Π΅ΡΠΆΠΈΠ²Π°Π΅Ρ ΡΡΠ±-ΠΏΠΈΠΊΡΠ΅Π»ΡΠ½ΡΠΉ ΡΠ΅Π½Π΄Π΅ΡΠΈΠ½Π³ ΠΈ Π½Π΅Ρ Π½ΠΈΠΊΠ°ΠΊΠΎΠΉ Π²ΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎΡΡΠΈ Π΅Π³ΠΎ ΠΎΡΠΊΠ»ΡΡΠΈΡΡ. ΠΡΠ»ΠΈ Π²Ρ ΡΠΈΡΡΠ΅ΡΠ΅ Ρ Π½Π΅ΡΠ΅Π»ΡΠΌΠΈ ΠΊΠΎΠΎΡΠ΄ΠΈΠ½Π°ΡΠ°ΠΌΠΈ, ΠΎΠ½ Π°Π²ΡΠΎΠΌΠ°ΡΠΈΡΠ΅ΡΠΊΠΈ ΠΈΡΠΏΠΎΠ»ΡΠ·ΡΠ΅Ρ Π°Π½ΡΠΈ-Π°Π»ΠΈΠ°ΡΠΈΠ½Π³, ΡΡΠΎΠ±Ρ ΡΠ³Π»Π°Π΄ΠΈΡΡ Π»ΠΈΠ½ΠΈΠΈ. ΠΠΎΡ Π²ΠΈΠ·ΡΠ°Π»ΡΠ½ΡΠΉ ΡΡΡΠ΅ΠΊΡ ΡΡΠ±-ΠΏΠΈΠΊΡΠ΅Π»ΡΠ½ΠΎΠΉ ΠΏΡΠΎΠΈΠ·Π²ΠΎΠ΄ΠΈΡΠ΅Π»ΡΠ½ΠΎΡΡΠΈ ΠΈΠ·Β ΡΡΠ°ΡΡΠΈ Seb Lee-Delisle: […]
2 FPS on ipad2 (4.3.5)
[…] For more info about this optimisation see this blog post. […]
Having done it before, I would advise AGAINST attempting to do a game in the DOM instead of using a CANVAS tag. There are extreme difficulties when it comes to getting consistent positioning of sprites between different browsers, even if you use position:absolute.
Browsers have no respect for your game’s coordinate system. The appropriate top and left attributes to use to position a sprite may depend on the user’s Zoom setting. The numbers reported by things like window.innerHeight also depend on the user’s Zoom setting. The size of your image as reported by its width and height properties, OTOH, may or may not be affected by the user’s Zoom setting.
There is also no way to reliably mix DOM objects with a CANVAS. Mario may fall into a pit on Chrome but fall into the solid ground to the left of the pit in Firefox.
There is no way to programmatically test of any of this in JS.
DOM rendering is so fucked up, that you really shouldn’t try doing anything more than simple documents with it.
[…] to spot a difference between rounded- and non-rounded pixel-values while it is (on most platforms) way faster for the canvas to render on pixel instead of subpixel values. So: use integer-values or for EaselJS-Objects: myObject.snapToPixel = true; – it is faster […]
What about aliasing when scaling sprites? I’m using drawImage with all the parameters to scale sprites and if I scale the sprites up there is a lot of smoothing going on. Any way to get rid of that?
Drawing without anti-aliasing is surprisingly difficult! Here’s a great article all about it : //phoboslab.org/log/2012/09/drawing-pixels-is-hard
[…] interpolates with the (for instance) 2D API. The interpolation and antialiasation processes will slow the performance down. Therefore always try to give non-floating numbers to a 2D context method. A positive side-effect […]
[…] Ein wesentlich wichtiger Punkt, den man sich merken sollte wenn man mit Canvas arbeitet, ist: Canvas-Koordinaten sind Integer oder, passender formuliert, ganze Zahlen! Obwohl das Drawing API auch FlieΓkommazahlen als gΓΌltige Werte entgegennimmt, merkt man spΓ€testens beim Zeichnen, dass sich hier einige Frames gewinnen lassen. Sprich, vor jedem Zeichnen sollten die Koordinaten und LΓ€ngenangaben zu ganzen Zahlen gewandelt werden. Positiver Nebeneffekt: Die Kanten der gezeichneten Elemente bleiben scharf [10]. […]