Reusing previously placed in-memory strings that go outside the screen when scrolling is a widely used technique to optimize the use of the ListView component, originally implemented in iOS and Android. The default implementation of ListView as a component of React Native does not directly contain this optimization, but has a number of other nice advantages. However, this is an excellent sample worth exploring. Considering this implementation as part of the React study will also be an interesting thought experiment.
Lists are an important part of mobile application development.
Lists are the heart and soul of mobile apps. Lots of apps display lists: this is the list of posts in your Facebook app feed, lists of conversations in Messenger, a list of Gmail emails, a list of photos on Instagram, a list of tweets on Twitter, etc.
As your lists become more complex, with a significant number of data sources, thousands of lines, media files requiring large amounts of memory, their development also becomes more difficult.
')
On the one hand, you want to maintain the speed of your application, because scrolling at 60 FPS has become the gold standard for native interaction experience (UX). On the other hand, you want to keep memory consumption low, because mobile devices do not have excessive resources. It is not always easy to fulfill both of these conditions.

Search for the perfect implementation of the ListView
The fundamental rule of software development is that no optimization can be foreseen for any scenario.
Consider an example from another area: there is no database that is ideal for storing any data. You may be familiar with SQL databases, which are great for some use cases, and NoSQL databases, which are optimal for other situations. You are unlikely to develop your own database, therefore, as a software developer, you need to select the appropriate tool for solving a specific task.
The same rule applies to the list view: you can hardly find a way to implement the list view, which would not only be suitable for any use case, but also maintain a high FPS speed and low need for memory.
Roughly speaking, there are two types of options for using lists in a mobile application:
• Almost identical strings with a very large data source. Contact list lines look the same and have the same structure. We want users to quickly browse the lines until they find what they are looking for. Example: address book.
• Strongly different strings and a small data source. Here, all the lines are different and contain a different amount of text. Some contain media. In most cases, users will read messages one by one, and not view the entire stream. Example: chat messages.
The advantage of separating into different use cases is that you can offer different optimization techniques for each option.
Ready view of React Native lists
React Native comes with a great ready-made ListView implementation. It contains some very reasonable optimizations, such as “lazy loading” of rows appearing on the screen when scrolling, reducing the number of redraws to a minimum, and drawing lines in different event cycles.
Another interesting feature of the finished ListView implementation is that it is fully implemented in JavaScript over the native ScrollView component that is included with React Native. If you have had experience developing for iOS or Android, this fact may seem strange. Their native SDKs are based on the time-tested implementations of list views - UITableView for iOS and ListView for Android. It is noteworthy that the React Native team decided not to use any of them.
There may be various reasons for this, but I assume that this is related to previously defined use cases. UITableView for iOS and ListView for Android use similar optimization techniques that work fine for the first use case — for lists with almost identical rows and a very large data source. The finished ListView React Native view is optimized for the second option.
The main list in the Facebook ecosystem is the Facebook post feed. The Facebook application was implemented in iOS and Android long before React Native. Perhaps, initially, the tape implementation was really based on the native UITableView implementations in iOS and ListView in Android, and as you can imagine, it didn’t work as well as expected. The tape is a classic example of the second use case. Lines are very different, because All publications are different - they differ in the volume of content, contain different types of media files and have a different structure. Users consistently read publications in the feed and usually do not scroll through hundreds of lines at a time.
So why don't we consider reuse?
If the second use case — lists with very different strings and a small data source — fits your needs, then you should consider choosing a ready-made ListView implementation. If your case is described by the first use case, and you are not satisfied with the work of the finished implementation, we would recommend experimenting with alternatives.
I remind you that the first use case is lists with almost identical lines and a very large data source. For this scenario, the main optimization technique that has proven its effectiveness is string reuse.
Since our data source is potentially very large, it is obvious that we cannot store all strings in memory at the same time. To minimize memory consumption, we will store only those lines that are currently displayed on the screen. Lines that are no longer visible as a result of scrolling will be freed, and new lines that become visible will be placed in memory.
However, for the constant release and placement in memory of the lines when scrolling requires a very intensive work of the processor. Using this native approach, we may not reach the desired speed of 60 FPS. Fortunately, in this use case, the lines are almost the same. This means that instead of releasing a line scrolled off the screen, we can create a new line from it, simply replacing the data displayed in it with data from the new line, thereby avoiding new allocations in memory.
Let's move on to the practical part. Let's prepare an example to experiment with this use case. The example will contain 3,000 lines of data of the same structure:
import React, { Component } from 'react'; import { Text, View, Dimensions } from 'react-native'; import RecyclingListView from './RecyclingListView'; const ROWS_IN_DATA_SOURCE = 3000; const dataSource = []; for (let i=0; i<ROWS_IN_DATA_SOURCE; i++) dataSource.push(`This is the data for row # ${i+1}`); export default class RecyclingExample extends Component { render() { return ( <View style={{flex: 1, paddingTop: 20,}}> <RecyclingListView renderRow={this.renderRow} numRows={dataSource.length} rowHeight={50} /> </View> ); } renderRow(rowID) { return ( <Text style={{ width: Dimensions.get('window').width, height: 50, backgroundColor: '#ffffff' }}>{dataSource[rowID]}</Text> ); view rawRecyclingExample.js hosted with by GitHub } }
Using the native implementation of UITableView
As noted earlier, native SDKs for iOS and Android have robust implementations that perform string rewriting. Focus on iOS and use UITableView.
You may wonder why we are not trying to implement this technique entirely in JavaScript. This is an interesting question that deserves a detailed description in several separate blog entries. However, in short, in order to rewrite the lines properly, we must always know the current scroll offset, since when scrolling, the lines must be overwritten. Scroll events occur in the native zone, and to reduce the number of transitions through the
RN-bridge , it makes sense to track them in it.
Objective-C: #import "RNTableViewManager.h" #import "RNTableView.h" @implementation RNTableViewManager RCT_EXPORT_MODULE() - (UIView *)view { return [[RNTableView alloc] initWithBridge:self.bridge]; } RCT_EXPORT_VIEW_PROPERTY(rowHeight, float) RCT_EXPORT_VIEW_PROPERTY(numRows, NSInteger) @end
The wrapper itself will be executed in RNTableView.m, and will mainly deal with passing properties and using them in the right places. There is no need to go into the details of the next implementation, as it still lacks some interesting parts.
#import "RNTableView.h" #import "RCTConvert.h" #import "RCTEventDispatcher.h" #import "RCTUtils.h" #import "UIView+React.h" @interface RNTableView()<UITableViewDataSource, UITableViewDelegate> @property (strong, nonatomic) UITableView *tableView; @end @implementation RNTableView RCTBridge *_bridge; RCTEventDispatcher *_eventDispatcher; NSMutableArray *_unusedCells; - (instancetype)initWithBridge:(RCTBridge *)bridge { RCTAssertParam(bridge); if ((self = [super initWithFrame:CGRectZero])) { _eventDispatcher = bridge.eventDispatcher; _bridge = bridge; while ([_bridge respondsToSelector:NSSelectorFromString(@"parentBridge")] && [_bridge valueForKey:@"parentBridge"]) { _bridge = [_bridge valueForKey:@"parentBridge"]; } _unusedCells = [NSMutableArray array]; [self createTableView]; } return self; } RCT_NOT_IMPLEMENTED(-initWithFrame:(CGRect)frame) RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder) - (void)layoutSubviews { [self.tableView setFrame:self.frame]; } - (void)createTableView { _tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain]; _tableView.dataSource = self; _tableView.delegate = self; _tableView.backgroundColor = [UIColor whiteColor]; [self addSubview:_tableView]; } - (void)setRowHeight:(float)rowHeight { _tableView.estimatedRowHeight = rowHeight; _rowHeight = rowHeight; } - (NSInteger)numberOfSectionsInTableView:(UITableView *)theTableView { return 1; } - (NSInteger)tableView:(UITableView *)theTableView numberOfRowsInSection:(NSInteger)section { return self.numRows; } -(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { return self.rowHeight; }
The key concept is to combine the native environment and JS
We want our strings to be React components defined in JavaScript, because that is all the business logic. But we also want them to be easily customized. Since the real rewriting logic works in the native environment, we need to somehow “transfer” these components to it from JS.
It is best to pass the React components as children to our native component. Using the native component from JS, adding the strings to JSX as child components, we force React Native to convert them into UIView views, which will be presented to the native component.
The trick is that you do not need to create components from all rows of the data source. Since our main goal is to reuse strings, only a small amount is needed to display on the screen. Suppose that the screen simultaneously displays 20 lines. This value can be obtained by dividing the screen height (736 logical points for iPhone 6 Plus) by the height of each row (in this case, 50), getting an approximate value of 15, and then adding a few additional lines.
When these 20 lines are passed to our component as children of a subview for initialization, they are not displayed yet. We keep them in the bank "unused cells."
This is followed by the most interesting. Native rewriting in UITableView works using the “dequeueReusableCell” method. If a cell can be overwritten (from a string that is not displayed on the screen), the overwritten cell can also be returned using this method. If a cell cannot be overwritten, our code will have to put a new one in memory. The placement of new cells occurs only at the beginning, before we fill the screen with visible lines. So, how to put in memory a new cell? We will simply take one of the unused cells in our bank:
- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex { // subview, // [super insertSubview:subview atIndex:atIndex]; [_unusedCells addObject:subview]; } - (UIView*) getUnusedCell { UIView* res = [_unusedCells lastObject]; [_unusedCells removeLastObject]; if (res != nil) { res.tag = [_unusedCells count]; } return res; } - (UITableViewCell *)tableView:(UITableView *)theTableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *cellIdentifier = @"CustomCell"; TableViewCell *cell = (TableViewCell *)[theTableView dequeueReusableCellWithIdentifier:cellIdentifier]; if (cell == nil) { cell = [[TableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier]; cell.cellView = [self getUnusedCell]; NSLog(@"Allocated childIndex %d for row %d", (int)cell.cellView.tag, (int)indexPath.row); } else { NSLog(@"Recycled childIndex %d for row %d", (int)cell.cellView.tag, (int)indexPath.row); } // … return cell; }
The final element of our puzzle will be filling the new overwritten / created cell with data from the data source. Since our rows are components of React, we will translate this process into React terminology: we need to assign new properties to the row component based on the correct row from the data source that we want to display.
Since the change in properties occurs in the JS environment, we need to do this directly in JavaScript. This means that you need to return a binding for one of our series. We can do this by passing an event from the native environment to JS:
This is a complete implementation of the function, contains all the missing parts. - (UITableViewCell *)tableView:(UITableView *)theTableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *cellIdentifier = @"CustomCell"; TableViewCell *cell = (TableViewCell *)[theTableView dequeueReusableCellWithIdentifier:cellIdentifier]; if (cell == nil) { cell = [[TableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier]; cell.cellView = [self getUnusedCell]; NSLog(@"Allocated childIndex %d for row %d", (int)cell.cellView.tag, (int)indexPath.row); } else { NSLog(@"Recycled childIndex %d for row %d", (int)cell.cellView.tag, (int)indexPath.row); }
Connect all together
Next, for the final implementation of RecyclingListView.js, we need to bind our native component in JavaScript:
import React, { Component } from 'react'; import { requireNativeComponent, View } from 'react-native'; import ReboundRenderer from './ReboundRenderer'; const RNTableViewChildren = requireNativeComponent('RNTableViewChildren', null); const ROWS_FOR_RECYCLING = 20; export default class RecyclingListView extends Component { constructor(props) { super(props); const binding = []; for (let i=0; i<ROWS_FOR_RECYCLING; i++) binding.push(-1); this.state = { binding: binding
Another optimization that we want to add is to minimize the number of redraws. Those. we want the line to be redrawn only when it is rewritten and the binding changes.
For this we need a ReboundRenderer. The parameter of this simple JS component is the index of the row of the data source to which this component is currently bound (the “boundTo” parameter). It is redrawn only when the binding is changed (using the standard optimization shouldComponentUpdate):
var React = require('React'); var ReboundRenderer = React.createClass({ propTypes: { boundTo: React.PropTypes.number.isRequired, render: React.PropTypes.func.isRequired, }, shouldComponentUpdate: function(nextProps): boolean { return nextProps.boundTo !== this.props.boundTo; }, render: function(): ReactElement<any> { console.log('ReboundRenderer render() boundTo=' + this.props.boundTo); return this.props.render(this.props.boundTo); }, }); module.exports = ReboundRenderer;
A fully working example, containing mostly the code provided here,
can be found in this repository .
The repository also contains descriptions of several other experiments that may interest you. The
tableview-children.ios.js experiment also applies to this case.
Tal Kohl is a full-stack developer specializing in the development of native mobile applications for iOS and Android. React Native - his new hobby. Tal was a co-founder of two technology companies, one of them now belongs to the platform for creating websites Wix.com.Original article:
Wix engineers blog .