Making a 60fps Mobile App
One of the challenging parts of being in Developer Relations is staying relevant. You can track the bleeding edge of browsers, write all manner of blog posts and articles, you can go ahead and talk at conferences. The important thing is to actually try out your own advice before you give it. That's exactly what I did.
To start, here are some screen grabs:
So with that out of the way...
The Plan #
The plan to keep me nice and relevant was very simple: create a mobile web app. As someone who trains regularly I decided to create an app to track my weight. There are many examples of native apps that do this, so I thought I'd give it a go! I also had a few additional goals in mind:
- 60fps at all times. I wanted to prove that the web doesn't have to be juddery, janky and slow. At least on a modern smartphone.
- Do one thing really well. In this case allow someone to track their weight. There are some similar apps on Google Play and the Apple App Store and they do this one thing.
- Offline support. I don't see why an app like this should require a connection beyond the initial download.
- Flat UI. It's all the rage and I wanted to give it a whirl.
So with that all laid out, here are the things I thought went well, things that didn't and what I learned through the whole experience.
Getting to 60fps #
Getting the app to run at 60fps was extremely important to me. Despite some difficulties I achieved it for the newest phones. (I got very close depending for older handsets.) I actually developed it against a Galaxy Nexus, on which it ran just fine, though I don't think it quite hit 60fps.
Interestingly the problem you're going to have if you measure it in DevTools (which is exactly what I did) is that the act of observing changes the observed. More crucially, because I'm often right up to the 60fps limit, adding the overhead of DevTools busts my frame budget. If you simply look at the app when DevTools is capturing and when it isn't you can actually see the performance difference. Without DevTools capturing things are far smoother.
Getting the app to run at 60fps was extremely important to me.
The slightly lower overhead method I used to verify that I was getting 60fps was adb_trace, which is maintained by my colleague John Mccutchan. It lets you grab a trace in a very similar much as you would with about:tracing in desktop Chrome.
In any case there is rarely any perceptible lag and, while that's not a scientific measurement, it's a pretty fine empirical measurement while you're adding features.
Try the app #
It works on Nexus devices running Chrome, and on iPhones.
Sadly there seems to be a bug with the Samsung and HTC handsets, which I hope to work around soon, that causes the <canvas>
to have rendering issues :(
It's also not styled for desktop or tablet, so bear that in mind!
So how to get to 60fps? Here are some of the tactics I employed, most of which you will recognise if you've read my other blog posts or articles on HTML5 Rocks:
- I made my
<canvas>
elements larger than 256px. This triggers the hardware path for Chrome. Some people are frustrated by that, but remember that just because something is using the GPU doesn't magically make it faster. Some things are faster on the CPU. In this case my app wasn't, so I bumped my<canvas>
elements' dimensions up. - I promoted each "view" of my app to their own layer with
-webkit-transform: translateZ(0)
. I knew that I didn't want them grouped to a single bitmap under the hood and that I would be usingopacity
to fade them in. I could've probably avoided the belt-and-braces of thetransform
because a transition onopacity
should trigger a layer creation, but if nothing else it'll remind me when I come back to the code. - I initially placed my canvas elements on semi-transparent layers so you could see the underlying views. That caused my composite times to go through the roof. They're still extremely high as-is anyway, but by placing them on fully opaque backgrounds it seemed to reduce them somewhat.
- I only transitioned views on opacity to avoid paint.
- I used the
<canvas>
for visually updating items. As it happened I designed the app so that a<canvas>
would make the most sense, but generally in my experience updating DOM elements such that a paint is required slows things down enormously. This appears to be due to the paint workload and limited texture bandwidth available for updating bitmaps on the GPU. It's a shame, but doubtless over time this will get better. - I didn't use any MVC libraries. (For what it's worth I used Paul Irish's rAF polyfill and a font loader lib.) The reason is pretty simple: most libraries would've been a hammer to crack a nut for such a small app, and, crucially, I wanted to be absolutely certain that if there was a performance issue it was me and not them.
Other stuff that worked out fine #
AppCache turned out to be no major issue to work with, because a) I was briefed by Jake Archibald and b) it was a single page app. With that said I still managed to get myself locked in, especially so on iOS, and no amount of pleading would get the cache to purge.
I ended up removing the app from my home screen, killing both the app and Mobile Safari and purging its cache all in an attempt to force the issue. I dread to think what a typical user would have to do to make an AppCached app to play out well.
I dread to think what a typical user would have to do to make an AppCached app to play out well.
The flat UI stuff seems to have gone down well with most people. I've had a lot of nice remarks (thanks!) so that's a nice outcome.
Things that didn't play out well #
In no particular order:
- I dislike that we don't have any orientation lock for the web. If you make a native app it's the easiest thing in the world to specify supported orientations. Try doing that for the web. I think we need that ability.
- Despite building for Chrome on Android differences in GPU drivers appear to cause issues on some Samsung and HTC devices. Run that sucker on a Nexus device and it's fine. Same goes for iPhones 4S and 5. (As an aside I didn't realise how relatively cheap the Nexus 4 is, having never been much of an Android guy. Thinking my next smartphone might not be iPhone 5 now.)
- It felt like a real shame that I had to change my design. especially dropping some of the semi-transparent stuff, but when push came to shove, better performance won.
Things I Learned #
Overall then here's what I thought. (I know this post is list heavy, but stuff it let's have one more!)
- DevTools Overrides are super useful as a first step for rapid prototyping of features and styling.
- You simply can't build a meaningful mobile experience without frequently testing and profiling on the actual devices you support. That is challenging for most people as they can't buy every device under the sun. I have no good answer here!
- You don't always need a ton of libraries for a small app.
- It's remarkably difficult to build a 60fps app; you can shoot yourself in the foot very easily. I had to wrangle the app a lot to get there.
- No matter how many features you include or exclude there will always be someone who wants one other "small feature" to be added. Fact.
A final thought #
Once I'd finished I noticed that I was comparing my app to the native ones on my iPhone. In fact I've started using it as my primary weight tracker, not because I built it, but because it's genuinely useful to me.
So really, when performance issues disappear you are left not with lingering thoughts on how the app was built, but rather the user's experience, and that's ultimately what matters.