Recently we released the Flickr Lightbox for iPad, iPhone and Android. We managed to create a pretty responsive interface. It took us a while to get there, and we learned a lot doing it. In this post we’d like to share a few useful key lessons we took away from the project.
Get Your Viewport Tag Right
The viewport tag is actually not that well understood, but you have to get it right, or everything is just going to be completely confusing. For the lightbox it looks like this:
<meta name="viewport" content="width=device-width; initial-scale=1.0; maximum-scale=1.0; user-scalable=0;">
This is the way we made sure that we could reliably position elements by pixel, and properly handle device rotation without fear. Other places on the web describe the details of how this tag works, but for us the most important was “maximum-scale=1”. This is because when you rotate the iphone it scales the content normally, unless you specify a max scale. Positioning becomes very difficult when the phone is scaling the interface you carefully crafted to fit on the device.
Simplify the DOM
On mobile WebKit you can use CSS for much, much more than you can on the desktop where you need to support bad browsers. This makes it very easy to clean the DOM down to the minimum. We did this on the lightbox by starting from scratch (the DOM for the desktop lightbox is quite complex to support a cross-browser layout.) We also actually nuke the existing DOM when the lightbox opens.
Keep It Responsive
I can not emphasize this enough. Of course, any UI must be responsive. But a touch UI doubly so. Apple clearly “got” this in the development of the first iPhone. Possibly because there is no “click” sound when you interact with the device it is very hard to tell if the device has registered your interaction without immediate feedback. Further, if there is even a tiny delay in how the device responds to touch interaction it feels clunky. Much clunkier than a slightly glitchy desktop UI. One of the things keep us from supporting the desktop lightbox on touch devices was that it felt very slow and clunky. Once we figured out that this was a matter of percieved responsiveness it was pretty clear where we needed to focus: percieved performance.
From a user experience standpoint this means that any interaction needs to give the user feedback. In the lightbox this means that when the user swipes the photo always moves with their finger. When the user hits the end of the photos, rather than not responding anymore, the photo continues to move but snaps back (with CSS transitions) to the last point.
Of course, for this interaction to work it needs to be very fast, any delay feels very awkward. So from an optimization standpoint we went after the performance of the swipe animation above everything else. The first thing to do is to use 3d CSS transforms. All the touch devices have 3d acceleration hardware which makes it possible to move the photo much faster than with just the CPU. The additional benefit of course is that when using transforms the animation does not block JS execution at all.
The code looks something like this:
distance = e.touches[0].pageX - startX; absDistance = Math.abs(distance); direction = (absDistance === distance) ? 1 : -1; if (absDistance &gt; 2) { thisPosition.setStyle('transform', 'translate3d('+distance+'px, 0px, 0px)'); }
When we first tested this, however, we found performance quite disappointing. Another team pointed us to a trick: don’t use <img> tags. We got a huge performance boost when using <div> tags with the photo as a background image.
The next thing we noticed was a slight but perceptible delay between the touch event and the movement of the photo element. After some profiling we found that the YUI event abstraction was actually taking enough time to be perceptible, so we switched to native event handling. Which lead us to further optimization along the same lines: do as little as possible. Most things you do in JS (with some special exceptions) are blocking. So any work you do while touch events are being handled necessarily delays the feedback the user needs to know that their touches are being registered.
We went through the code path that happened during touch events. Anything that could wait until “touchend” was deferred there.
The last problem to solve was that the browser would crash with more than twenty or so slides loaded. It seems that the iPhone browser dies very quickly when it runs out of memory, especially when using 3d acceleration. So we implemented a simple garbage collector for the slide nodes:
//remove all slides more than 10 positions away function pruneSlideNodes() { if (inTransition || moving) { if (pruneHandle &amp;&amp; pruneHandle.cancel) { pruneHandle.cancel(); } pruneHandle = Y.later(500, this, pruneSlideNodes); //wait return; } positionManager.each(function (value, key) { if ((Math.abs(key - parseInt(currentPosition,10)) &gt; 10) &amp;&amp; value.id) { Y.one('#' + value.id).remove(); delete(value.id); } }, this); }
Final note:
Moving to YUI 3 has been huge for us, even on mobile tasks. The mobile lightbox takes advantage of several modules created for the desktop, most importantly a “model” module we created to manage what we call photo “contexts”. This meant that the logic of displaying slides is the same in all places, the view/controller code was all that we needed to create for this. Which also means that this logic exists in just one file.