OPTIMIZE PERFORMANCE FOR LISTVIEW IN REACTNATIVE

ListView is a common UI element in mobile applications.

React Native provides its own version of ListView, it is essentially different from Android’s ListView or iOS’es UITableView.

Android ListView and iOS UITableView cache the list item or cell for visible rows. Let’s say if you have a list of 1000 rows and only 10 rows are visible at a time, then the native list will create just enough for the rendering and reuse them.

That’s why scrolling through a list view on native platforms are very smooth. (It has its own drawbacks, eg it’s tricky to correctly maintain and update internal state of the cells)

React Native’s ListView uses the flexbox layout style, it doesn’t support caching. It does however provide some optimization options:

  • Only re-render changed row with rowHasChanged. This is the main focus of this blog post
  • By default, only one row is rendered per event loop. This break the heavy load into chunks and retain smoothness.
  • removeClippedSubviews offscreen child views are removed.
  • Built-in pagination support

This post will show you how to properly implement rowHasChanged to prevent over-rendering. It also propose a reference to pagination implementation to prevent overloading.

Simple ListView

The code below implements a simple list of posts. Each post has a title, description and like button. We use Faker library to generate the dummy text content.

let seed = times(
  20,
  () => ({
    title: Faker.Lorem.sentence(),
    description: Faker.Lorem.paragraph(),
    liked: false
  })
)

export default class Posts extends Component {
  constructor(props) {
    super(props)

    const ds = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 })

    this.state = {
      posts: seed,
      ds: ds.cloneWithRows(seed)
    }
  }

  _toggleLike(title) {
    const posts = this.state.posts.map(post => ({ ...post }))
    const idx = posts.findIndex(post => post.title === title)
    posts[idx].liked = !posts[idx].liked

    this.setState({
      posts: posts,
      ds: this.state.ds.cloneWithRows([...posts])
    })
  }

  _renderRow(row) {
    console.log('render row')

    return (
      <View style={ styles.row }>
        <Text style={ styles.title }>{ row.title }</Text>
        <Text style={ styles.desc }>{ row.description }</Text>
        <TouchableOpacity
          style={ styles.like }
          onPress={ () => this._toggleLike(row.title) }
        >
          <Text>{ row.liked ? 'Unlike' : 'Like' }</Text>
        </TouchableOpacity>
      </View>
    )
  }

  render() {
    return (
      <ListView
        style={ styles.container }
        enableEmptySections={ true }
        automaticallyAdjustContentInsets={ false }
        dataSource={ this.state.ds }
        renderRow={ row => this._renderRow(row) }
      />
    )
  }
}

We put a log in the render row function to see how often it is called.

  _renderRow(row) {
    console.log('render row')
    ...
  }

If you compile and run it, it seems fine. The list is rendered correctly, you can click on the Like button to toggle the status.

There is however a problem with the rendering. Our list has 20 rows, every time you click on Like/Unlike button all of the rows are re-rendered and logs out render row 20 times. This seems unnecessary as only 1 row changes.

What is up here?

The list datasource has a function to decide whether a row has changed.

new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 })

Basically when it returns true, the row gets rerendered.

The condition r1 !== r2 seems good, except that we clone a new post object when the user clicks on Like / Unlike

  _toggleLike(title) {
    const posts = this.state.posts.map(post => ({ ...post }))
    const idx = posts.findIndex(post => post.title === title)
    posts[idx].liked = !posts[idx].liked

    this.setState({
      posts: posts,
      ds: this.state.ds.cloneWithRows([...posts])
    })
  }

We create a whole copy of post list in the state, as a result r1 !== r2 always return true for each of the row. That’s why all 20 rows are re-rendered.

Improvements

To prevent unnecessary rendering, we need to improve the comparison conditions.

Based on the UI, each item is rendering title, description and like status. When any of these changes, we’ll update:

_rowHasChanged(r1, r2) {
  return r1.title !== r2.title || r1.description !== r2.description || r1.liked !== r2.liked
}

Now run the project again, and like one of the posts, only that row is re-rendered. The rest remains intact.

This applies to any other list, you only need to re-render to update visible changes.

Pagination

This usually has significant performance effect for both mobile and backend.

The app shows only the first page and load more as users scroll.

ListView has built-in properties like onRefresh or onEndReached to facilitate pagination. You just need to fetch the next page when onEndReached is triggered.

We have another blog to show how to implement pagination in ListView. Please have a look here

Here is the source code of this project https://github.com/CodeLinkIO/ReactNative-ListView-Performance

Khac Anh is a full-stack engineer with an extensive background in web and mobile development. He has built and lead tech teams in several outsourcing and startup companies, one of which was successfully publicly listed.
You might also like...
To stay productive in programming you need to receive feedback for your code as fast as possible. For example, when you want to test out some styling effects with CSS...
Basically it's where you deploy your code to. Using a PaaS has several benefits over Infrastructure-as-a-Service (IaaS) or setting your own physical servers...
Spelling suggestion is pretty convenient for users to search for similar content. It accept a search query with typo mistake and suggest the correct one...
contact codelink

Contact

Let us know how we can help!