Richard Scarrott

Frontend Web Developer, London

Rendering Big Lists in React

LOADING

Richard Scarrott on the 2nd August, 2016

React has become increasingly popular largely because it offers a great abstraction allowing you to conceptually re-render your UI on every state change. This means you can forget about writing state transition logic and only ever need to describe your UI at a single point in time, which turns out to usually be really very easy.

This abstraction is founded on the assumption your UI will only ever be made up of a limited number of nodes and in practice this is more often than not a fair assumption. Having said that, it is definitely still possible to blow your rendering budget when working with particularly large data sets.

Fortunately, in many cases these large data sets don't need to be represented on the screen all at once, so you can get away with 'windowing'; only rendering the nodes currently in the viewport. This is a technique commonly employed by native apps allowing them to maintain 60fps however, it is notoriously difficult to implement, especially on the web and especially when targeting underpowered devices.

Then there are other times where you may justifiably need to render the entire data set in view at once meaning windowing just isn't an option.

For these scenarios you need to instead focus on simply reducing the amount of time it takes to render every node. For this React provides a leak in its abstraction in the form of shouldComponentUpdate, which when combined with persistent immutable data structures, allows you to prune subtrees during re-renders based on knowledge that state hasn't in fact changed.

This can be really very effective, as more often than not state changes are performed on discrete portions of your state, which in turn only effect discrete portions of your component tree, so there's often a lot of opportunity to prevent 'wasted' renders.

Unfortunately, when rendering large lists shouldComponentUpdate often isn't able to sufficiently prune large enough subtrees due to the inherent high degree of the root node:

LOADING

You can see here the degree of the root node is 12, so given a state change at this level all 12 leaf nodes would be re-rendered, or at the very least shouldComponentUpdate would be evaluated, which is by no means free. You can easily picture how expensive it could get as your list grows in size.

Conversely however, if you were to reduce the degree of the root node by inserting a smaller number of internal nodes, which in turn each render a subset of the leaf nodes you'll find (although you've now got a slightly bigger tree) you have more opportunities to prune larger subtrees with shouldComponentUpdate, which ultimately results in fewer nodes being created:

LOADING

Here you can see a render is now able to ignore many of the leaf nodes (those in white) turning what was a linear time render into logarithmic time. This could quite easily be the difference between hitting 60fps or not.

How can we reduce the degree of the root node?

So, the downside to this is your state tree needs to reflect this new component tree, for example where you may have previously modelled a list of users as a flat array or object like this:

{
   users: {
      1: 'Hamilton',
      2: 'Ricciardo',
      3: 'Verstappen',
      4: 'Rosberg',
      5: 'Vettel',
      6: 'Raikkonen',
      7: 'Hulkenberg',
      8: 'Button',
      9: 'Bottas',
      10: 'Perez',
      11: 'Gutierrez',
      12: 'Alonso',
      ...
   }
}

You would now have to model each of the new internal nodes by storing the users as multiple nested objects:

{
    users: {
        1: {
            1: 'Hamilton',
            2: 'Ricciardo',
            3: 'Verstappen',
            4: 'Rosberg'
        },
        2: {
            5: 'Vettel',
            6: 'Raikkonen',
            7: 'Hulkenberg',
            8: 'Button'
        },
        3: {
            9: 'Bottas',
            10: 'Perez',
            11: 'Gutierrez',
            12: 'Alonso'
        }
    }
}

It's obviously not always ideal to model state in this way, as CRUD operations could become more expensive and sorting made difficult however, there are occasions where it can make sense semantically. For example, a large form where the nested objects represent 'fieldsets', or perhaps a list of products returned from a paginated API where the nested objects represent 'pages'.

Additionally, some of these pain points could potentially be alleviated through the use of normalizr, as it makes it easy to model the actual entities themselves as a flat object allowing the nested objects to simply maintain references.

One last caveat worth mentioning, as it currently stands, is each internal node will result in an extra wrapping element rendered to the DOM, which could restrict the ability to accommodate certain responsive layouts but hopefully this will be addressed with work being done on Reacts new reconciler.