Official Blog - ticket news, tour previews & company announcements

Building the T!ckets App Part 2: Horizontal Lists, List Performance, Animations and Profiling

Updated May 1, 2020 / by Keith Hanson
Originally Published May 24, 2016

This is the second in a three-post series about the RateYourSeats.com team's experience using Ionic and AngularJS to build the T!ckets app.

Jump to Part 1

Jump to Part 3

Keith Hanson will share his thoughts on horizontal lists, list performance, animations and overall performance profiling.

----

Use angular-swiper For Horizontal Lists (Think Cards)

One of the most attractive aspects of Ionic is the way the framework manages lists. Right out of the box, Ionic Lists are aesthetically appealing, easy to implement and versatile. Dividers, icons, avatars and thumbnails are literally just a CSS class away. And because they look so good, you won't feel the need to over-write the default stylesheet too often.

ionic lists
Out-of-the-box lists in Ionic

These vertical lists are awesome on the phone, intuitive and deployed in almost every mobile app. But for us - in our industry - they were deployed in TOO many apps.

We researched the apps of eight of our closest competitors and EVERY single one implemented the same boring list of events when you open the app. These types of lists have their place in event exploration apps, but they offer very little in the way of organization and no hierarchy.

Because we're not focused on event exploration, we wanted to get our users to a specific sports team or performer as quickly as possible. Using horizontal lists would allow our users to see our logo, a search box, nearby teams and nearby performers in a single glance of the home screen.

tickets app
The home screen of the T!ckets app

Horizontal Lists/Cards With Ionic

While we're happy with the finished product, finding the right solution for a horizontal list within Ionic took some trial and error. To begin, we played around with Ionic's horizontal scroll. But it doesn't come with the bells and whistles (think snapping, auto-calculation, etc.) that make horizontal lists user-friendly and visually appealing.

When searching the Ionic Marketplace, we found what appeared to be a perfect match in Horizontal Scroll Cards. There's a very good codepen for this product so it was easy to get up and running. Looking back, I wish it was a little more difficult to implement - maybe we would have given up on it earlier. It's not that it didn't work - it just doesn't have any documentation. Fortunately for us, when we started looking for some support, we stumbled upon angular-swiper.

Meet angular-swiper

Angular-swiper is an extension of Swiper, which lives up to its name as the "Most Modern Mobile Touch Swiper". If you're building in Ionic, you'll need to follow installation instructions for the angular version, but the entire non-angular Swiper API is available to you. This includes a full suite of callbacks, methods and parameters.

To demonstrate how easy it is to implement, here is the actual HTML/AngularJS used to create the horizontal scroll cards for the "Popular Teams" list from the home view screenshot above.

angularjs code
The HTML used to generate horizontal scroll cards

That's all there is to it; no more than 10 lines of code in my HTML plus three lines (not shown) in my controller to register the swiper.

The swiper is highly performant, it snaps into place nicely when you swipe half-way, and as you'll see later on, we implement it in A LOT of different ways!

Performance Issues on Android: Scrolling Vertical Lists

While vertical lists weren't a practical solution for our home screen, they are obviously the best approach for large lists where we offer our users a way to self-organize (sort and filter).

There are two instances in our app where we show large lists of data. The first is when we present a list of events for a performer, and then second is when we show a list of tickets for an event/performance.

ionic large lists performance
Large lists created a performance issue within the app

Some performers, like Blue Man Group, have up to 1,200 events active at any given time. And some events - especially NFL games - have up to 3,000 ticket listings. When dealing with large lists, there are warnings all over the Ionic Community about list performance - especially as it relates to scrolling. According to the community, using angular's ng-repeat on lists of more than 60 items will result in slow and choppy scrolling.

Aside: It's worth noting that performance complaints vary by device, operating system and software version. This is why we were careful to describe hybrid apps as having no "global" performance limitations in our opening post. If this is confusing or concerning, please let us know in the comments!

Are We Cooking Magic Grits?

So here we are, creating lists with ng-repeat that have 50 times the recommended number of items. As we assume is the case with most Ionic developers, we were using ionic serve and emulating a mobile device through Chrome. When we tested our massive ticket lists, we had no performance issues what-so-ever. Obviously, we're just better at writing ng-repeats than all other Ionic/AngularJS developers.

Our proficiency was proven true when I first put the app on an iPhone 6 with Ionic View. When looking at events with ~700 ticket listings, scrolling was very good. Keep in mind, at this point I'm using ng-repeat on a complicated ion-list which includes a thumbnail photo in each ion-item.

Next step? Open up the same event page on an Android running KitKat 4.4.2. The results? Let's just say that we're not cooking magic grits. The laws of Ionic/Angular physics apply to us as well.

Scrolling through the 700-item ticket list was painfully slow. We noticed slightly better scrolling performance (less lag) on an Android running newer software, but it was still only about 20-30% performant (where we would only ship something if it was 80% or above).

If you're running into similar issues and banging your head, don't be deterred! There are a number of potential solutions and hacks. We tested combinations of each of the following: lazy-loading the list, one-way bindings, removing images and collection-repeat.

Solution 1: Lazy-Load the List

Otherwise known as infinite scroll, this actually worked OK for us. It allowed us to keep using ng-repeat and was pretty straightforward to implement (see here for the tutorial we followed).

The tl;dr version goes:

  • Create ng-repeat to iterate over our ticket list
  • Add a limitTo filter to the ng-repeat
  • Set the value of limitTo equal to a scope variable
  • Pre-set that variable to some number (20 for us)
  • Detect when user scrolls to bottom of ticket list
  • Increase the value of the limitTo variable when user reaches list end

Improved Scroll, But a Noticeable Lag at the Bottom

When scrolling through the first 20 items it was quite performant (and even more-so when you use one-way binding). But when you reach the bottom of those 20 items and the limitTo gets updated, there was a noticeable lag in the lists we tested (we did not test lists with less than 300 items). Steve told me that he loves ripping through lists on mobile devices just to see how fast he can get it moving. With infinite scroll it was like being in a Lamborghini but hitting a red light every 1/8 of a mile.

Solution 2: One-Way Bindings

When users scroll through our ticket list, we wanted to be able to show them on the seating chart the location of all tickets visible in the list. This was a must-have for us but it also leads to significant performance challenges.

In order to get this working, we had to detect how far down a user had scrolled and then constantly update a variable that held a list of tickets that were "in view". In AngularJS, updating this variable means that we have to run a digest. In other words, the program will cycle through all of the variables (bindings) in a given view and check to see if they were affected by the change to the "in view" variable. So if Steve is ripping through the ticket list at Lamborghini speeds, we're asking the program to run these cycles frequently - sometimes multiple times per second. Even the most powerful smartphones begin to "redline" when the calculations are so intense and frequent.

To reduce the number of variables that need to be checked, we were directed to angular's one-time/way bindings. Essentially, you're telling the program that the data are static and they should not be checked for updates. For us, in a list of 700 tickets with 12 variables each, we had the potential to reduce our watch list by as many as 8400 items.

So how did it perform?

One-way bindings are really easy to implement using the {{::var}} format. This made testing a breeze. We noticed a 20-30% improvement when using one-way bindings with ng-repeat. This is great improvement on a semi-performant list, but when we're starting off at 20% performant to begin with, we've got a long way to go. Onto the next.

Solution 3: Removing Images From the List

As mentioned, our ticket list items include a thumbnail image to give users a small preview of their section view. For those looking to speed up list performance, removing images is a common suggestion. This was probably never going to be a permanent solution for us. We have the largest collection of in-seat photos and it's important for us to show them off with our ticket lists! But we were curious to test it out. We only saw a performance increase of about 10%. For us this was good news because it meant that the photos weren't slowing us down that much*.

*Note, we are still using ng-repeat here. This is important to note because in some of our larger lists, we were showing hundreds of different thumbnails in the list. And in ng-repeat, ALL photos will be downloaded when the view is loaded. This led to incredibly slow view transitions.

Collection-Repeat

While collection-repeat shows up last on our list here, it was the first solution we tried - and likely the first solution you'll encounter if you do additional research on improving scroll performance.

As the ionic docs put it, collection-repeat allows an app to show a large list much more performantly than ng-repeat because it only needs to render as many items as are currently visible. This should sound pretty similar to our lazy-loading attempt. But instead of building a custom solution, the collection-repeat directive is all you need. Here's us using collection-repeat to show a large list of events:

angular collection-repeat
An example of using collection-repeat for scrolling performance

Collection-Repeat Limitations

As a refresher, scroll performance on large lists with ng-repeat graded out at about 2.5/10. Simply by swapping out ng-repeat for collection-repeat, we saw grades around 7.5 on older Android devices. As a single solution, collection-repeat is far better than one-time bindings, removing images and lazy-loading. However, there are some limitations:

Can't Use With Native Scrolling

We didn't discuss native scrolling as a potential solution to poor scrolling performance, but it is something you may consider using. Unfortunately, you cannot use native scrolling with collection-repeat.

Can't Use With One-time Bindings

This one came as a real bummer to us. The way collection-repeat renders only active elements, it cannot be combined with one-time bindings.

Here's a summary of the full path we took on our journey towards optimizing scroll performance on Android devices:

  • ng-repeat: 25% performant
  • collection-repeat: 75% performant
  • lazy-loading w/ng-repeat: 95% performant => 20% performant at end of list
  • one-time bindings w/ng-repeat: 45% performant
  • one-time bindings w/collection-repeat: does not work
  • native scrolling w/ng-repeat: 55% performant
  • native scrolling w/collection-repeat: does not work
  • collection-repeat w/max efficiency in bindings and digests (final): 85%

Continue reading to learn how to track your bindings and digests.

Page Transitions and Animations

One of the most exciting aspects of Ionic is getting your app on Ionic View for the first time. It feels so good to "game" the system by developing an app with HTML, CSS and JS. Your splash screen shows, your app's logo appears - and for the first time you really start to believe that this whole hybrid thing is going to work out.

So you open your app in Ionic View and when your app's home screen appears, you tap one of the links to another view. You think back to how beautiful the page transitions looked in your Chrome development and how no one will ever know this is a hybrid app. Then it happens. The transition is slow, choppy, janky. You quickly tap the back button thinking "must have been a one time thing with the API and server". So you tap another link from the home screen and the same thing happens. Janky to the max. Your excitement quickly turns to disappointment and your smile quickly churns out expletives.

If you haven't tested your app on an Android device yet (either through Ionic View or ionic run android), you haven't had the pleasurable experience described above. Don't worry, it's coming. It's just a matter of time. You'll soon find that animations and transitions on Android devices just aren't very good.

Choppy Animations

Our first iteration of the T!ckets app included one of those standard slide-in menus that everyone uses (and is super-easy to implement with Ionic). The menu uses a slide-in animation. Here's a YouTube video on how it should look:

And that's exactly how it looked for us - when we emulated through Chrome (as I believe the video producer is doing). Once the app was on a real Android device, smoothness gave way to choppiness. This is a well-documented issue, and one that has been receiving a lot of attention for nearly two years.

Limiting the Number of Animations

I won't bore you with the details of every plugin and hack we tested to make animations smoother. Instead, I'll simply say that nothing was good enough for us. Instead of implementing a two-bit hack, we decided to limit the number of animations in the app. For example, we scrapped the slide-in menu in favor of an ellipsized drop-down menu.

Modal Animations

I described earlier the large ticket lists in our app. Given the sheer number of tickets, it's important for us to allow users to organize (sort and filter) the listings. In our experience as mobile web developers modals are the best way to present sorting and filtering. In Ionic, this means dealing with the default slide-in and slide-down animations for modals. Like the slide-in menu, the sliding is choppy.

Showing modals is one of the few places where we still use animations. In Ionic, the default way to show these guys is an animated slide-up and slide-down.

Note: A resource we didn't try: ionic-animated-modal

One thing I'll point out here is that when you open a modal, a few digest cycles will run. We typically observed 3-4 digest cycles running each time we opened a modal. We feel like this is causing a lot of the choppiness, as we get more chop within views where we have a greater number of bindings. For example, if we have 2,000 bindings and three digest cycles run, the program has to make 6,000 checks over the course of the time the modal is opening (which should be instant). We feel as though these checks are slowing down the modal opening and causing the chop in the animation. Just another reason why you should be keeping a close eye on digest cycles and bindings.

Page Transitions

I described my first experience with page transitions a few paragraphs back. I noted how disappointing the experience was. The transitions were slow, choppy and simply, non-native-like. The complete opposite of this video:

When we were literally just days away from delivering the app, fixing page transitions was on our "nice-to-have" list. As we tested the app more and more, we found ourselves more and more disappointed by page transitions. Fixing the transitions soon went onto the "must-have" list, and we delayed the app for about a week to make wholesale changes.

Load Faster vs. Transition Smoother

As we dug in a little deeper, we noticed that there were two main issues. First, when we were going to a new view, the view would take a little while to load (think of a slow-loading webpage where you wait on the old page while the new page loads). The "little while" was directly correlated with the amount of data we were getting back from the server. Once the data was then painted to the new view, the transition itself was choppy. Again, more data = more choppiness. Unfortunately for us, limiting the amount of data we were sending was difficult. Instead, we sought a more elegant solution.

Receiving Lots of Data

The data for most of our views is served from an API. And there are varying amounts of data for each view. When showing a list of team names for Major League Baseball, for example, there are only thirty items and not a lot of data. However, when we're showing tickets for a MLB game, we may have 700 tickets - each with more than a dozen attributes. We also have the world's most powerful interactive seating charts with row-information and a very complicated back-end. It didn't take us long to realize that the MLB teams list was loading much quicker than the tickets list. Our API calls are relatively fast, but transferring the data and reading the data is dependent on how large the data is. So it was not surprising to us that views with less data would load fast.

Smooth Transitions

In the course of testing, we also noticed that the less data, the smoother the transition. There seemed to be a lot of things going on at once. The data was being organized onto the view, a number of digests were running on the new view load and it was incredibly choppy.

To over-come this issue, we decided to take advantage of Ionic's life-cycle events. Within your controller, you can trigger blocks of code at specific times in the view's life-cycle. For example, you can wait for the view to fully enter before executing some code.

What we wanted to do with the life-cycle events is essentially prevent all of the code (including API calls) from executing until a view was active/seen by the user. While preventing the code from running, we could transition in a view that had very few elements (typically just a template with a spinner in the middle). Because the view had fewer elements, the transition would be smoother. And because we're not running the API call until the view is loaded, there isn't any waiting/additional chop.

Using afterEnter

As mentioned previously, we wanted to wait until the view was active to the user. This led us to use $ionicView.afterEnter with the following logic:

  • Transition in a template with a spinner showing and other elements hidden
  • Detect when the view is active with .afterEnter
  • Execute heavy-lifting code (API calls, filling in template, etc.)
  • Hide the spinner and un-hide the rest of the view elements

With this logic transitions were much smoother!

Another Mirage

What appeared to be a major victory turned out to be a mirage. The afterEnter will get called every time the view becomes active. More specifically, if you go to a new view (move forward in the app) and then hit the back button, you're supposed to go back to your cached view (this is a major perk of Ionic). But if you have the afterEnter in your controller, you're telling Ionic that you don't care that the view is cached and you want to execute your controller code again. If you don't care about caching, this isn't a big issue. But if you want to eliminate unnecessary API calls and other code from running, you need one more piece of logic.

The Last Piece of the Smooth Transition Puzzle

In order to hack around the issue described above, we simply used another one of the life-cycle events: beforeLeave. What we're doing is setting a scope variable $isCached=true right before the user leaves to go to a new view. Since the view is cached (and so is the scope), when the user hits the back button we first check to see if it $isCached. If it's cached, we run no code and simply show the cached view*. Otherwise, this means the user is seeing the view for the first time (or he/she hit the back button 10 times - impossible in our app) and so we can take advantage of afterEnter. This ended up being a pretty good solution for us, but would love to hear if you have something else you use or if you use ionic-native-transitions.

*Even though the data is all filled in, transitions to cached views are pretty good

Still Room For Improvement

Despite the victories declared above, transitions and animations are what I would call the "weakest" part of our app. These are the two aspects where someone might be able to detect that our app is non-native. Here's how I rate transitions and animations in the live production app:

iOS Transitions: 8/10
iOS Animations: 9/10

Android Transitions: 7/10
Android Animations: 5/10 :(

It's worth noting that a number of native apps also struggle with transitions and animations. For example, we graded a number of travel apps with numbers similar to our app. We typically wouldn't ship a product with a score of 5/10 in any category. But when observing a number of huge national brands struggling with this same issue, we lowered our standards a bit.

Keeping Track of Digests and Bindings

If you have any performance issues what-so-ever, we strongly implore you to keep track of your digests and bindings.

When a digest occurs, the program will cycle through all bindings and check for updates. We used the ticket list example from earlier to show that when a digest ran, we were checking 12 variables (bindings) for each of 700 tickets. That's 8,400 checks. Most developers have been taught to freak out at 2,000 bindings.

What we found to be just as important as the number of bindings in a digest is the frequency of digests. To a user, the performance of 8,400 bindings being digested once every 30 seconds is going to be far superior to 400 bindings being digested every half-second.

Simplistically, a good way to start is by minimizing the number of digested bindings per second (dbps).

So if you have 400 bindings and they're digested 15 times per minute, on average, your dbps is 100. Though this is a rather crude statistic, you should attempt to minimize the result as much as possible within the context of your app. If you expect frequent user interaction, you will be running a lot of digests and you should try to limit your bindings. If you have large lists (like us) and a lot of bindings, try to be efficient with your digest cycles.

Our Method For Improving Performance

Because this was the first app we developed with Ionic, we had no clue what the performance was going to be like. There weren't a lot of great examples of known-Ionic apps that we could download and test (Sidenote: This is part of the reason for this post; we encourage you to download our app and see if the performance passes your standards!). So we decided to build the app to 100% with our inefficient, we-have-no-clue-what-we're doing ways. And then we deployed the app to our phones and tested the hell out of it.

Performance Grades

We created performance grades on a scale of 1 to 10 for a variety of functions on multiple devices. For example, when Steve first "finished" the maps interaction and integrated it with the rest of the app, I tested on devices I had access to and then sent a response email that looked like the below:

Steve then went to the drawing board, improved, pushed to Ionic View and we tested and summarized again. We did this a lot (perhaps too many times for Steve's liking).

Logging Your Efficiency

While most of our performance scores were based on personal experience vs. other apps, we were able to quantify our digests and bindings. Once we started to get really serious about improving our dbps, we would test the app in Chrome with the console open and the following code running:

ionic benchmarking code
The code we used for testing and optimizing the app's efficiency

We ripped the above code from somewhere. There are a number of duplicated copies floating out there now, but I wish I could link to the original source. Anyway, that code was stuck in our app.js, so it would show for every view.

That will display in the console the number of watchers (I've been known to call these "bindings" and sometimes "view variables") that are active and will also log any digest cycle. When we were really cranking away and focused on performance, it became a game to limit the number of active watchers and how often we were forcing digest cycles.

Once we placed that code in our app, not only were we able to make it more efficient, we understood angular and the digest cycle so much better. In any future apps we develop we'll have that code in from day 1! (Just remember to take it out when shipping the app - console.logs are very slow)

As an added bonus, tracking digests and bindings helps you test third-party plugins for performance. For example, the slider plugin we use for our ticket price filter would run a digest cycle for every one dollar change on the slider. So if a user was moving the slider from a budget of $100 to $50, it would run 50 digests in that time - with more than 3,000 active watchers! By keeping an eye on this, we were able to modify the plugin to limit the number of digests and to improve user experience.

Continue Reading

Part 1: Native vs. Hybrid and Choosing Ionic

Part 3: The AngularJS Mindset and Solutions For Ionic Gestures, Layered Touches and Custom Browsers

----

We'd love to hear your feedback on this post and your experience developing hybrid mobile applications! Please use the comments or send us an email to communicate with us.

Finally, please download the T!ckets app! Not only is the best way to search, compare and buy tickets - on your phone - it will really help to contextualize this post. The app is available in the App Store and on Google Play.