Background
Infinite List, Windowed List, and RecyclerView are not new. In fact, their ideas are rather simple: render only what’s visible from the user’s viewport, recycle all item view instances with a smart backing data store, and progressively re-render dirtied items based on the scroll position delta. But details matter, and it’s not often done efficiently, regardless of platforms.
Our Use Case
Chop is a mobile commerce startup in the pre-launch stage. Since our inception we chose React Native to develop our iOS prototype both for technical and business reasons:
We need Native features like Bluetooth, background tasks, notifications, and payment options on iOS. With the Progressive Web App the Web is getting there, it just isn’t universally available yet.
Prior to building Chop, I worked at Google and founded the Chrome Developer Relations team back in 2009, and remained there for over four years. During that time, I got to know the JavaScript developer community rather well. While the language has its own shortcomings, what I love most about JavaScript are the people behind it. Their creativity, passion, and eagerness to share are unrivaled. The system can be fragile from time to time, but the community reacts and responds quickly to any issues. It’s similar to a human body, with the right antibodies and the ability to self-correct it’s always strong.
We hope to develop on Android platform later and possibly a web version of Chop in the future. React Native allows us to to maximize code reuse across all platforms.
The ability to quickly change the styling of the app, and move components based on customer input is critical. React Native allows faster iterations than any other platform.
React Native combines fast execution with the best use of today’s developer ecosystem — making it a perfect platform for startups.
One feature of the very early Chop prototypes is an infinite scrolling list of cafes and restaurants, with estimated wait times at each location. It has a few characteristics:
Our list item is a fixed width and height
We know the size of the list ahead of time
The list should scroll fast without jank or high memory use
The estimated wait time circle needs to be self-contained so it doesn’t dirty the entire list when there is a change
Here is the demo of our infinite list implementation:
Here is the snapshot of CPU and memory usage through Xcode, both are pretty decent and more importantly the memory size stays consistent.
CPU usage for non-stop 2 minute scrolling
Memory usage for non-stop 2 minute scrolling, via xCode
iOS Infinite List Overview
Let’s first take a quick look how this is done on iOS natively:
iOS Infinite List Implementation
We are going to use a simplified version of the Chop ‘Explore List’ by holding five rows in memory, through UITableView, in a single column list. ContentOffset is a point within the entire content space that defines where visible rendering starts. Here it says rows five, six, and seven are visible on the viewport.
Typically, if the user scrolls down, row seven will scroll off the screen, while row four will scroll in. UITableView accomplishes this by adjusting the contentOffset range. Soon, however, if you keep scrolling you will reach the edge of the content.
A few years ago, Apple introduced a technique to achieve infinite scrolling. You can watch their video describing it here: http://devstreaming.apple.com/videos/wwdc/2011/104/refmovie.mov ( Safari only, at 4:26 mark).
Simplified, the first step is to re-adjust contentOffset to its original center state so the user won’t hit the edge. However, now row four is off the screen. To bring row four back to the screen you have to move all the rows down by one. If you do both operations in the same event loop it will be transparent to the user, and allow for infinite scrolling.
React Native Implementation
React Native is using a different optimization strategy, which Christopher Chedeau, explains well in this article.
Essentially, React Native uses ListView for infinite list but it has a number of drawbacks. The most important of which is that React cannot assume elements of the list are immutable, and therefore it has to keep a reference to every new element that is scrolled into view in order to perform change detection. Regardless of whether it does detach views when their offsets are out of the visible area, its memory representation of the list items remains in memory. Diffing gets more and more expensive over time as the list grows.
Let’s take a look at its implementation.
React Native Infinite List
If a user beings to scroll, row four becomes visible and row seven falls off the screen. React Native keeps both rows seven and eight in its memory in order to do the diff and change detection later. The list is essentially stateful. As the user continues to scroll, the component tree grows in size and doesn’t shrink. There’s a greater potential for memory loss depending on the quantity of rows in the list. ListView now includes an option to removeClippedSubviews, so what that does is keep native views in memory, but detached from the view hierarchy, which cuts down on unnecessary native views. However, this still retains references to all previously rendered rows.
Currently, Brent Vatne also has an experimental windowed ListView for fixed height list: https://github.com/brentvatne/fixed-height-windowed-list-view-experiment
Chop: Fixed Height Infinite List
We took a slightly different approach to better suit Chop’s functionality.
Since our application uses fixed-height rows that all follow the same template, and we know the height of our viewable area, we settled on a design that treats the ScrollView’s child elements as immutable, except for the top-level elements in the array, and the height of the ScrollView’s interior. All other attributes of the list and its items will remain constant once created.
Here’s a look at Chop’s specific implementation:
Chop’s Implementation — Fixed Postions
Chop’s infinite list can be thought of as a list of items with fixed positions. We designed it to use a sliding window to maintain a visible section of rows.When the user scrolls, the sliding window will move throughout the giant space the list is situated in, but the items will remain in their fixed absolute positions.
Because everything on the list is fixed in size, height and position, from React Native’s perspective there is no apparent diff. This means there is no need to hold more rows in the tree so there is no increase of the memory usage.
When the size of each item is known, you can compute the y-position of any element with a formula such as y-position = itemIndex * itemHeight. The body height of the ScrollView is always the highest y-position item plus its height.
Example
Pseudo-code
https://gist.github.com/viviancromwell/ab540606af94b2ff858c#file-infinitelist-onscroll-js
OnScroll: How to update the renderModel
https://gist.github.com/viviancromwell/fe9714582270250e2989#file-infinitelist-onrender-js
OnRender: How to rerender the Infinite Scroll Widget
If you examine the demo video of the infinite list, you can see that it does render mutated data, namely the Wait Time bubble. However, we are not relying on React to do change detection in this case. Instead, we use an observable view model to detect changes to our data items, and force the Wait Time bubbles to become dirty and re-render. This scopes down the amount of state that has to be retained and diff’ed in the infinite list.
The Need for a Fixed Height List
The majority of the practical use cases out there simply deal with fixed-size items. (App Store app list, iTunes music list, Yelp restaurant list, Netflix movie list, Amazon product list, YouTube video list, or a geographical map with infinite bi-directional scrolling via fixed-size tiling, etc.). A variable-size list is often needed for user generated posts, typically in social network apps such as Facebook and Twitter. However it’s not the most common use case, and the implementation should not solely account for the worst case scenario.
Hopefully, a high performance fixed-height list can be standardized in the React Native platform to handle these use cases.
Special thanks to Jacky Nguyen who was the architect of this design.
Try out Chop: https://goo.gl/g9M1EW