Collection Repeat: Estimate, Iterate, Improve
Update: Collection Repeat has been replaced by Virtual Scroll in Ionic 3+.
Collection repeat is Ionic’s buttery-smooth solution for scrolling huge lists. Inspired by iOS’s UITableView, we switch out elements as the user scrolls, so that only the minimum necessary elements are rendered. We released our first version of collection repeat last year and have been improving it since then. Recently, we identified some huge potential performance increases and decided a complete refactor was necessary.
Before we dive into the details, let’s talk about how collection repeat works at the most basic level.
Say there are four items on the screen, matching items 1-4 in the user’s array of data: As the user scrolls down, item 1 will move up and out of view. Once it’s fully out of view, its element will move to the bottom of the screen to the space where item 5 should be, as item 5 moves up and into view.
Additionally, we take the Angular scope that just represented item 1, assign item 5’s data to it, and trigger a digest on that scope to make the element represent item 5.
To follow the above strategy and position every element properly, we need every item’s exact width and height.
The Old and the New
In our first iteration of collection-repeat, we required developers to provide dimension stats for every item, because we assumed that every item might have a unique width and height:
<ion-item collection-repeat="item in items"
With the new syntax, height and width are optional:
<ion-item collection-repeat="item in items">
As you can see, in the common case where every item is the same size, you don’t have to provide dimensions at all. Collection repeat now shines as a drop-in replacement for ngRepeat.
See the documentation for more information.
The Problems with the First Iteration
The old collection repeat assumed that, in every case, any item in the list could be uniquely sized. This assumption required us to recalculate every single item’s width and height whenever the scroll view resized. This expensive operation caused unacceptable lag when loading or rotating the phone.
When we took another look at UITableView, we hit upon a better solution. UITableView accepts an ‘estimatedHeight’ for every element in the list and uses that to estimate the size of the scrollView. Then, while the user scrolls down, each item’s dimensions are calculated on demand, and the size of the scrollView adjusts to reflect the actual dimensions.
We realized how much this could help performance and went into refactor mode.
Improvements in the New
Instead of requiring the user to input estimatedHeight, we compute the dimensions of the first element in the list with getComputedStyle() and use that for the estimatedHeight and estimatedWidth.
This lets us calculate dimensions lazily. We estimate that
scrollView.height === estimatedHeight * items.length at the start, and as the user scrolls, we calculate the actual height of every element.
We also made some optimizations in the rendering of items. For example, we now batch DOM operations on items by setting cssText. We also now digest items entering items one frame after positioning them.
But the biggest optimization is in the calculation of dimensions. The new collection repeat has four possible ‘modes’ it enters, the first being the most performant, and the last being the least performant:
- Static List Mode: This mode is entered when the height is given as a constant or not given at all, and the width is 100%. Here, we assume the height of every element is equal to the estimatedHeight. The math for this mode is simple and easy because every item has the same dimensions.
Dynamic List Mode: This mode is entered when height is given as a dynamic expression, but width is still 100%. In this mode, every item potentially has a unique height. We get the computed height of the first item and use that to calculate the estimated size of the scrollView. Then, as the user scrolls, we lazily calculate the dimensions of the next items that should be shown.
Dynamic Grid Mode: This is the most complicated mode and is entered when both height and width are dynamic expressions. It’s the same as dynamic list mode, except we also have to account for a potentially unique number of items appearing in each row.
The problem with the old repeater was that it was always in Dynamic Grid Mode and calculated all dimensions up front. This led to worse performance while scrolling, loading, and resizing.
Now, even in the worst case of dynamic grid mode, collection repeat is more performant than ever.
More Performance Opportunities
The biggest remaining opportunity for more performance gain is in the iOS browser’s rendering of images.
Whenever you set the
src of an
img on iOS to a non-cached value, there is a freeze of anywhere from 50-150ms–even on an iPhone 6. In our tests, an Android 4.1 device with images in collection repeat outperforms an iPhone 6.
Images are very commonly used with collection repeat, and we change the
src of those images often as the user scrolls.
We tried creating a web worker that fetches the image, converts it, and sends its base64 representation back to the UI thread. The image is then set to this base64 representation as a data-uri. This fixes half of the problem. If you set an
img src to a data-uri that has been set before, it instantly gets the rendered image from the cache and shows it without lag. However, the first time a unique data-uri is set, there is a similar delay to that of a a normal
This is still an improvement from normal src, which just doesn’t cache well at all.
We’re experimenting with a few more tricks to improve iOS performance, and plan to release them as a simple-to-use solution soon. We welcome your ideas.
Where We Are Now
The new collection repeat is better than ever, and easier to use than ever. Give it a try!
View the documentation at http://ionicframework.com/docs/nightly/api/directive/collectionRepeat.