Lessons Learned from the Flickr Touch Lightbox

Bye bye Kodachrome
Bye bye Kodachrome by e_pics

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 > 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;&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)) &amp;gt; 10) &amp;amp;&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.

Flickr flamily floto

Like this post? Have a love of online photography? Want to work with us? Flickr is hiring engineers, designers and product managers in our San Francisco office. Find out more at flickr.com/jobs.

The Joy of Popup Windows

The Joy of Popup Windows

As you’ve probably noticed, we’ve been working on the login process here at Flickr. In an effort to make this flow as painless as possible we’ve moved to a “contextual” login, which is to say, logging in doesn’t require you to leave the page you are looking at. We accomplished this by using a popup window.

We initially considered using an in-page modal dialog box instead of a popup, but quickly dismissed this approach for two security reasons. Firstly, to prevent phishing and cross site attacks it’s very important that login forms are not posted cross domain (Yahoo!’s Membership Platform authentication happens on yahoo.com, not flickr.com). Secondly the URL of the login page should never be hidden from the user.

An in-page modal fell foul of both of these concerns. A more suitable solution would be the popup browser window, once a favorite of advertisers.

Our initial approach

Revisiting our turn of the millennium Javascript for opening windows with the BOM, things looked simple enough: call window.open, make sure the window opened, then set a timeout to see if the window was closed:

function waitForWindowClose() {
	if (web1999Window && web1999Window.closed) {
		// the pop-up was closed, done with auth etc.
		window.location.reload();
	} else {
		// check again in a moment
		setTimeout(waitForWindowClose, 1000);
	}
}//cute function name by Scott Schiller
function webCirca1999(url) {
	try {
		web1999Window = window.open(url,'newWindow','width=640,height=650');
	} catch(e) {
		//handle blocked popup...
	}
	try {
		//this is to check if it really opened
		if (web1999Window.focus) {
			web1999Window.focus();
		}
	} catch(ee) {
		// also handle failure
	}
	waitForWindowClose();
	return false;
}

Unfortunately Yahoo!’s new user signup process closed our popup and opened a new one of their own. This triggered our waitForWindowClose{} function, web1999Window && web1999Window.closed evaluated to true, and the Flickr page refreshed, even though the user hadn’t logged in yet.

The next thing we tried was watching window focus. When the window blurred after clicking on the login popup, we’d know the popup was open, and when focus came back we’d check the user’s login credentials.

But focus tracking also had its problems. Some browsers didn’t always report the window blur event when a popup was open, resulting in inconsistent behavior. It was also possible for the user to accidentally focus back on the window and kill the flow. We tried several ways to track focus, and although behavior got better, it was never 100% reliable. For a feature like login that was obviously a problem.

The Cookie Solution

Ultimately we settled on a solution that forgot about popup tracking and window focus and instead concentrated on checking for the user’s login cookies. The code below polls to see if the login cookie is set, then makes an AJAX request to verify it, then refreshes the Flickr page:

function pollCookie(){

	if(document.cookie.match(sessionRegex)){
		authInProgress = false;
		checkAuth();
	}else{
		Y.later(20, this);
	}

}

This has the benefit of being very responsive, avoids the nightmare of focus tracking, and is low impact (as there’s no need for us to poll the server).

So today we have a reliable contextual login. Our popup system also made it possible to add google and facebook login with less pain than usual.