Every website wants to have a flashy front page. It increases user interaction, raises revenue, and adds the all-important awesome factor to your site. For Zoosk, this means giving users the ability to swipe between search results, just like in a native app. But making one isn’t always the simplest task. As part of a recent project, I implemented the swipe ability, and the story is a long one.
I started this project by thinking, “what would the most Angular way to do this be?”. The answer that I found was using a custom directive, like this:
[gist id=400bbec1cd5262618312 file=one.html]
The result of this code would be a full-page slideshow that would repeat the inner DOM three times. The middle slide would display the current search result, and swiping left or right would make the slides on the left and right visible. But how to do it?
AngularJS tries hard to keep controllers and DOM separate. The recommended solution for situations like this is a directive, but if you’ve tried changing the document structure in your directive’s link function, you’ll find that it doesn’t always work. Instead, you need to use the compile directive parameter.
[gist id=400bbec1cd5262618312 file=two.js]
Using the compile function, it isn’t too hard to copy the innerHTML of the element and then append the appropriate DOM. The most important part, though, is saving your elements for later.
Having the slideshow is cool, but how to make each slide display the correct search result? First of all, you need to get your hands on the children’s $scopes. And since you can’t guarantee that your child elements have a scope to begin with, you’re probably better off actually associating a new scope with each of your child slides as they are appended:
[gist id=400bbec1cd5262618312 file=three.js]
This ensures that each of your slides has its own isolated scope and will remain working after it is appended to the slideshow container. Once that happens, you’ll be able to let each slide know about a different slideshow index, and have them handle the rest. You might want to simply:
[gist id=400bbec1cd5262618312 file=four.js]
Or, you could do what I did and use an event that will allow controllers to react to new indices:
[gist id=400bbec1cd5262618312 file=five.js]
Either way, after you’re all done you’ll have each slide displaying the correct information. Now all we have to do is make the swiping work!
What is probably the most important part of the Angular swipe experience ends up hardly using Angular code at all. Your code will differ depending on your CSS setup, but here’s the general idea of the code that you’ll put in your directive’s link function:
[gist id=400bbec1cd5262618312 file=six.js]
At this point, I had a working slideshow. Hooray! Except that on my iPhone, it was about as quick as a paralyzed turtle. It turns out that the road to getting JS in a mobile browser to perform at native speeds isn’t pretty.
You’re probably familiar with the idea of a graphics card. It makes things look amazing, but it eats up battery power like a famished hippo. As a result, animations and transitions can be choppy in the browser. However, there is one thing that the GPU does do better: 3D transforms.
But wait, you say, we don’t want anything 3D to happen here. That’s why we’re going to trick the browser into rendering our slideshow element with the GPU by applying this transform:
[gist id=400bbec1cd5262618312 file=seven.css]
This transform moves the page in 3D space… by 0 pixels. This produces no change, but it does cause the GPU to draw the element, which means that any translations applied to it will be smooth as silk. The line about backface-visibility causes the browser to skip drawing the back side of the page, which is useless to us.
You may have noticed that AngularJS picks up changes almost magically. This is because of its system of watch statements, which goes over everything in the DOM and looks for changes after anything happens. This makes development very quick, but can lead to performance problems when implementing large pages.
Our architecture calls for having three pages all working at once, and all those watch statements contributed to a 650ms lag on finger release in the first implementation. For the second one, I created a new “dummy” page that looked like the search result page, but didn’t have anything but the display logic in it. These dummies were displayed as the slides adjacent to the middle one.
This reduced the number of $watch statements by more than 50%, and performance subsequently went up.
After the above optimizations, the site performed like a dream on iOS. Android, however, was a different story. The CSS transition seemed fine, but the dragging behavior was still laggy.
After doing some research, I found that there was a bug in Android mousemove events. Particularly, that they don’t fire nearly as quickly (sometimes 5x) as iOS.
The upshot of this is that when dragging, the view wouldn’t update its position nearly as quickly, preventing anyone from experiencing a good frame rate. With a little more research, though, I found the solution:
[gist id=400bbec1cd5262618312 file=eight.js]
Even after the above improvement, Android phones seemed to underperform when compared to iOS. So I went into research mode, and came up with these rather unfortunate articles on Android performance and OS adoption rates. Apparently, the majority of Android users are not only using technology that’s slower than the competition, but they’re also neglecting important upgrades.
In a last-ditch effort to improve performance for the old platform, I added three new optimizations:
With all of these upgrades, we finally got our swipe numbers up to near the level of iOS. . . for Android 4.4 users. 4.3 and below were given up as a lost cause, considering that their graphics processing is on the level of the iPhone 3.
After a few days of performance tweaking, swipe was ready to go live. If you’re on a mobile device, head over to the zoosk.com mobile site to check it out!