Performance problems with React when using a big list

(Flo) #1

I hope this is the correct place to ask my question. I got a serious performance problem with React and it’s related to React concepts, so I think having a forum thread is better suited than StackOverflow.

My React web app has a list component which consists of 300 item components. There’s always one selected item and the selection can be changed by using the arrow buttons. So the state of the list consists of an array of data for items and the index of the selected item. Because the arrow buttons change the selection state, React rerenders the list whenever the selection changed. I implemented shouldComponentUpdate for the ListItem component and whenever the selection changes, that method is called for all 300 ListItems. My method implementation does two simple comparisons but because React has to call that method 300 times, the app becomes really slow. It takes about 200-300ms to update the selection, which feels really slow when I keep pressing the down button.
All ListItems also have a key property that’s set correctly.

So my first question would be if this makes sense so far? I’ve did a lot of testing the last hours and by now I’m pretty to sure that this is the performance bottleneck. However it still seems weird because the rerendering doesn’t really take longer if I don’t implement shouldComponentUpdate and let React decide if it should rerender the instance to the DOM. But maybe this is just because the ListItem component is pretty small, I also logged that my shouldComponentUpdate works correctly.

This performance issue feels really stupid to me because I always know which two ListItems would need to be rerendered (the one that lost selection, the one that got selection, the other 298 items will stay the same).

I found this SO question where someone had a very similar problem but the accepted answer uses undocumented ImmutableJS methods and the GitHub issue related to that is still open.
Do you guys know any good ways to solve this? I really like React but this is a pretty big problem for me.

(Bruno Mota) #2

Can you share some code? Hard to spot anything without it. I think that update time is not normal. Having shouldComponentUpdate implemented should make it much faster. Are you binding some function on render?

(Flo) #3

Yeah, the list component passes a callback to each item component that allows the item to gain the selection. In the item render method I then bind that method to onMouseEnter (an item should gain selection if the user moves their mouse over the item).

The method that renders the items returns this:

<Item key={} i={i} data={item} isSelected={isSelected} becomeSelected={this.props.changeSelection} />

I’ve used the React performance tools and the onMouseEnter is only bound once for every ListItem (regardless of how often I change the selection).
Here are some other results from the performance tools:


This is after I changed the selection 4 times. The amount of ListItems instances makes sense, assuming that each time the properties change it’s counted as a new instance. 300 initial instances and 4*2 more because of the change of selection.
I’m not that sure about the amount of MainView/ListView instances. Does it count as a new instance if a child component is rerendered? i.e. is this an expected amount of instances or is something going wrong here?

(Alex Guerra) #4

It’s really hard to say without more code. Any shouldComponentUpdate function (that usually returns false) should be a major performance improvement, since it shortcuts rendering and diffing. Maybe try running it with react developer tools closed?

Anecdotally, I have a similar component that handles cursoring, selection, drag and drop, and scrolling into view as you cursor, and I’ve seen it work without any performance issues with over 400 elements, so I don’t think you’ve just hit a wall with React.

(TomW) #5

Are you running the code in production mode? (e.g. NODE_ENV=production npm run build)

React does some things much faster in production mode, due to leaving all the complex debugging checks. 300ms seems way too long to carry out 300 simple shouldComponentUpdate checks.

That said, I think it would still be too slow once you get to more like 1000+ sub-elements. An obvious solution for React would be for render() to support returning an array, or some other way of splitting up a large number of flat dom children so they don’t all have to be rendered by a single function (hence allowing shouldComponentUpdate interception at a higher level). The other approach is to only render what is visible (like facebook’s FixedDataTable), so you aren’t trying to render 1000+ children.

(Flo) #6

Hey guys, thanks for the replies. I’ve finally found out what’s causing the problem. However I don’t have enough React knowledge to know why.

Anecdotally, I have a similar component that handles cursoring, selection, drag and drop, and scrolling into view as you cursor, and I’ve seen it work without any performance issues with over 400 elements, so I don’t think you’ve just hit a wall with React.

After reading this, I built a minimalistic standalone demo that also performed well enough with 1000 items. After that I kept debugging my app and in the end I found that the key property caused the problem. If I remove it, the update gets performed in reasonable time.
As soon as I add the key property, adding/removing shouldComponentUpdate won’t have any influence on performance.

As far as I understand React uses the key to distinguish between child components. The key s I used were unique IDs from a SQL DB, so I know for sure that they were unique and always stayed the same for the same item.
I also tried using the items text content as a key, and that had the same performance penalty. However using the index from the .map loop didn’t (but I guess that would be a bad key anyways).

Do you guys know why that behavior is happening? I’m still pretty new to React but it really surprised me that key caused such a bad performance.

(Alex Guerra) #7

Heh, if anything key should improve the performance. That using the index as a key worked well indicates to me that your unique ids aren’t staying the same or aren’t in the same order across renders for some reason. I could be wrong; I haven’t done any benchmarking on key vs no key for vary large arrays of components.

I’m not sure exactly what you’re doing, but if the items in your list don’t change much after initialization then the index is probably a good enough key and you can just move on.

(Bruno Mota) #8

Is it possible that the IDs you were using in the key were not a string? That is strange

(Flo) #9

Yeah, the IDs were numbers, not strings.
So I just tried casting all of them to strings before the React view gets them and it improved performance a lot, for 230 list items the updating used to take 200ms on average, with the IDs as strings it’s down to 132ms on average.

Why is that though? Did React cast all 230 IDs to strings on every state update and that caused the extra 70ms?

I’m not sure exactly what you’re doing, but if the items in your list don’t change much after initialization then the index is probably a good enough key and you can just move on.

The list renders the last n entries from a database and it’s possible to search the list which will usually result in new list items. So just using the loop index as a key wouldn’t work with searching.

By the way, is it possible to tell React to keep certain elements in the DOM? Everytime the search is reset React will rerender the entire list, although 99% of the elements will be the same as before the search.

(Alex Guerra) #10

This struck me as weird, since I remember seeing numbers explicitly allowed as keys on the virtual dom terminology page. In lieau of reading the source, I made a suuuuuuper ghetto test to confirm, and looks like you’re right; numeric keys are slower than string keys. Weird…

You could do this using css by setting display: none on the non-visible elements.

(Flo) #11

Thanks for posting the performance test, it’s good to know that it wasn’t just me doing crazy stuff somewhere :slight_smile:

To give a short update: I ended up changing the ListView to only render the list items that are visible. This fixed all performance problems I had. For nearly all state updates the rerendering time is down to ~30ms now.
In the beginning I was a bit reluctant to do it this way, but it actually made a lot of sense considering that there are at most 15 list items visible at once.

(Michel Weststrate) #12

For people finding this topic later, if you need to work with large lists and you are in a place where you cannot show just a subset, MobX might just help you out, see these stats

(Junior201110) #13

I’ve a problem like this. But I’ve been use array index as my key… I think that this is the problem. Array index can not be used as component key and use other index as key (like an ObjectId from MongoDB) is the rigth.