My friend and fellow Chicagoan Evan Miller wrote an excellent blog post over the weekend: Why I Develop For The Mac. It's full of great reasons why his software (which is also excellent, by the way) was written for the desktop, despite the fact that he's a web developer, even the creator of an Erlang web framework.
But I'm compelled to respond to it, specifically his statements about <canvas>:
large <canvas> areas seem laggy on most browsers
So I'm left with <canvas>, and <canvas> is slow.
I have become intimately familiar with <canvas> while developing Soundslice. I'd even venture to say Soundslice is one of the most advanced uses of <canvas> on the web that's not a tech demo -- i.e., it's an application that normal people use. The site uses not one, but nine <canvas> elements stacked on top of each other to make a very rich UI, sort of like Photoshop for guitar tabs. (For a flashy demo of how those canvases interact, watch the tech talk I gave at 37signals, specifically the bit starting at 10:20.)
Here's what I've learned: <canvas> is not slow. In fact, I've been continually surprised by how fast it is -- as long as you take care to do things right. Evan's article mentions the "magical" sensation of instantaneous feedback; I invite you to play with the zoom slider on any Soundslice page (example) to experience this same magic, all drawn dynamically with <canvas>.
Of course, <canvas> is certainly not as fast as the lower-level drawing routines that you can use if you develop a desktop app. No question. But it's fast enough that, unless you're doing something relatively insane, you'll be totally fine.
On Soundslice, we're drawing guitar-chord charts completely on the fly (again, see an example), which is a relatively involved drawing routine -- and it's still near-instant performance. That's across all modern browsers (Chrome, Safari, Firefox and IE 10).
Here are some specific tips I've picked up to make <canvas> performance really shine.
Above all else, do this.
For example, say you have an event such as mousemove that results in a <canvas> redraw. A mousemove might happen hundreds of times per second, but your screen might only refresh, say, 75 times per second (75 Hz). That means, if your code is naively written, it will try to redraw several times within each actual opportunity to redraw (hundreds of times per second vs. 75 actual redraw opportunities per second).
The requestAnimationFrame API solves this by letting you say, "Execute this code the next time a redraw happens." Which saves your browser from having to do unnecessary work.
When I added this to Soundslice, the site became dramatically faster and more responsive. Here's more info about how to use the API.
Above, I linked to a video of a tech talk I gave about Soundslice. In that talk, I demonstrated how Soundslice uses several different <canvas> elements, stacked on top of each other as layers, for maximum performance -- and for nice, clean code. Definitely watch the demo at around 10:20 in the video to get a sense of it.
I'm planning to write a separate blog post about this, but the Cliff's Notes version is that you can stack transparent <canvas> elements on top of each other so that you only have to redraw the ones that need to change.
For example, on Soundslice, there's a separate <canvas> for the playhead -- the vertical orange line that tracks the currently played moment of the video. That's a separate <canvas> with a z-index above the other ones, so that redrawing it doesn't require redrawing any of the other stuff. The less you have to redraw, the better.
Bunch calls to fillStyle
When you draw on <canvas>, you first have to tell it which color you're using. You can do that by setting the "fillStyle." It turns out that, each time you change the fillStyle, there's a slight performance penalty. Therefore, you can squeeze out some extra performance by bunching your calls to fillStyle -- that is, rather than drawing a gray thing, then an orange thing, then a gray thing again, you should draw all the gray things, then draw all the orange things.
For example, Soundslice, which is all about annotating YouTube videos, needs to draw dozens, sometimes hundreds, of annotations on the screen at a time. Each annotation might use several different colors -- the text color, the border color, the line color, etc.
My original implementation looped over each annotation and drew each one independently, which resulted in two to five fillStyle calls for each annotation. I changed this to bunch the fillStyles across all annotations -- so that all of the light grays were drawn at the same time, then all the dark grays, etc. -- and the drawing got a few dozen milliseconds faster.
For more background, see the "Avoid unnecessary canvas state changes" section in this great HTML5 Rocks article.
Cache text rendering
In profiling, I've found that rendering text on <canvas> is my next big rendering-related bottleneck on Soundslice. I haven't done this yet, but I'm planning to come up with a way of caching the results of fillText, possibly using this technique.
A decent argument in Evan's favor is: "Well, if <canvas> is only fast if you use these various hacks, it's not really fast, then, is it?"
Two thoughts on that.
First, well, sure! I'd love it if <canvas> was super fast right out of the box, without needing to use these techniques. No doubt about it. But the reality is, it is fast enough, if you put in the work.
Second, there's the bigger question -- a defining question for the current generation of web developers -- which is: web or native app? I am squarely in the web camp, both for philosophical reasons (such as openness) and practical reasons (such as the fact that Soundslice has only one developer and one designer, and we can't justify building separate apps for separate platforms).
What I love about <canvas> is that it lets us make desktop-quality apps right in the browser, so we can get the benefits of being "of the web" along with the benefits of amazing, fast graphics. Fear not, my friends: <canvas> is great.
UPDATE, May 7, 2013: Evan has posted a thoughtful follow up, reacting to this.