So many great minds in one room

I’m on my flight back from ReactConf 2017. Spending 3 full days with many giants of our industry has been one of the most inspiring experiences I’ve ever had. I finally got to place real faces on all the profiles I follow on Twitter!

I’ve always been intrigued by the challenge of ListViews in React Native. And what do you know.. I’m in the room with 3 of the most influential people in this space: Brent Vatne — one of the leaders of Expo and a driving force behind the React Native community. Spencer Ahrens — from the Facebook React Native core team, who recently released FlatList. And Brian Vaughn — the person behind React Virtualized, the best implementation I’ve seen for this problem for the web.

So what remains to be done?

There are many use-cases for ListViews, and different use-cases require different optimizations. Current implementations in React Native deal well with complex lists having a wide variety of cells, that are scrolled without jumping around too much. Compare the Facebook Feed to your phone’s Contact List.

When a user has a few hundreds or thousands of contacts and is browsing in attempt to find a specific one, they use the list quite differently. The scroll pattern would be more erratic, with large leaps and faster swipes . If we borrow from another world, you could say the user is trying to do random access instead of sequential :)

This is also where React Native has the most difficulty. Scroll takes place in the native realm, but rendering of the rows takes place asynchronously in the JavaScript realm. Data between them is queued on the bridge. If you scroll fast enough, render requests will eventually wait in line, resulting in blank spaces midst scroll.

What can we do to help?

Fiber is believed by many to be promising — with the ability to control render priority and cancel pending renders that are no longer needed. It is definitely interesting, but we’re going to experiment with something else entirely.

We’re going to try to cut the bridge out of the equation altogether. We’ve taken this approach before when dealing with other performance problems. Animations can finally run at 60 FPS using Animated’s native driver that minimizes bridge traffic… I’ve just shown a cool approach in ReactConf for doing user interactions at 60 FPS with a declarative physics library… We know that declarative API can be a powerful weapon.

If there’s no bridge, there are also no renders from JavaScript while we’re scrolling. This means we’ll have to rely entirely on recycling old rows. This is a standard approach in native ListViews like the iOS UITableView. Luckily, the Contact List scenario is also perfect for recycling — the rows are nearly identical. We can use this for our advantage.

Loyal followers among you may remember that I’ve already played with this approach about 9 months ago (see Recycling Rows For High Performance React Native List Views). The problem with the previous attempt was that we still relied on React and JavaScript to reconcile row content updates.

How can you update rows without React?

Simple. We’re just going to update rows without React.

If you look closely in the React Native docs, this ability was actually documented a while ago under the name “Direct Manipulation”. This API has never found its place and pretty much disappeared from the world. But digging through the old NativeMethodsMixin code can shine some light on how this can be done.

The plan

First, in our library, we’re going to use React to render a pool of rows that is just enough to cover one screen-fold. This will only happen during initialization so there’s no impact on scroll performance. Using React at this point will provide the developer with the flexibility to define the layout of the row template with JSX.

Next, we’re going to ask the developer to define the row template in JSX and declare explicitly how data from the data source will bind into it. This is where the declarative API comes into play:

If you look closely at the example above, you’ll notice we’re using TextInput components to display strings instead of using Text. There’s a reason behind this that we’ll go into a bit later (it will be resolved, no worries).

Our declarative API uses ref to define how various field IDs from the template bind into props of specific components. In this case, we bind the string template fields to the “text” prop of TextInput.

The next thing we’re going to do is pass the entire data source to the native realm. This may sound scary at first but will actually make very little impact on performance. Even with 5000 contacts, the total amount of data in the data source is small. Every contact has a couple of strings attached to it. The bridge has very high throughput, so sending it all at once during initialization will not make a big impact:

Next, we’ll ask the user to provide another declaration. This time of how fields from the data source map to fields IDs in our template. We’ll add this as another prop given to BindingListView:

As you remember, when we defined the data source above, each row in the data source had two fields: “initials” and “name”.