Mitch Karajohn Mitch Karajohn - 4 months ago 53
Javascript Question

Big list performance with React

I am in the process of implementing a filterable list with React. The structure of the list is as shown in the image below.

enter image description here

PREMISE

Here's a description of how it is supposed to work:


  • The state resides in the highest level component, the
    Search
    component.

  • The state is described as follows:




{
visible : boolean,
files : array,
filtered : array,
query : string,
currentlySelectedIndex : integer
}



  • files
    is a potentially very large, array containing file paths (10000 entries is a plausible number).

  • filtered
    is the filtered array after the user types at least 2 characters. I know it's derivative data and as such an argument could be made about storing it in the state but it is needed for

  • currentlySelectedIndex
    which is the index of the currently selected element from the filtered list.

  • User types more than 2 letters into the
    Input
    component, the array is filtered and for each entry in the filtered array a
    Result
    component is rendered

  • Each
    Result
    component is displaying the full path that partially matched the query, and the partial match part of the path is highlighted. For example the DOM of a Result component, if the user had typed 'le' would be something like this :

    <li>this/is/a/fi<strong>le</strong>/path</li>

  • If the user presses the up or down keys while the
    Input
    component is focused the
    currentlySelectedIndex
    changes based on the
    filtered
    array. This causes the
    Result
    component that matches the index to be marked as selected causing a re-render



PROBLEM

Initially I tested this with a small enough array of
files
, using the development version of React, and all worked fine.

The problem appeared when I had to deal with a
files
array as big as 10000 entries. Typing 2 letters in the Input would generate a big list and when I pressed the up and down keys to navigate it it would be very laggy.

At first I did not have a defined component for the
Result
elements and I was merely making the list on the fly, on each render of the
Search
component, as such:

results = this.state.filtered.map(function(file, index) {
var start, end, matchIndex, match = this.state.query;

matchIndex = file.indexOf(match);
start = file.slice(0, matchIndex);
end = file.slice(matchIndex + match.length);

return (
<li onClick={this.handleListClick}
data-path={file}
className={(index === this.state.currentlySelected) ? "valid selected" : "valid"}
key={file} >
{start}
<span className="marked">{match}</span>
{end}
</li>
);
}.bind(this));


As you can tell, every time the
currentlySelectedIndex
changed, it would cause a re-render and the list would be re-created each time. I thought that since I had set a
key
value on each
li
element React would avoid re-rendering every other
li
element that did not have its
className
change, but apparently it wasn't so.

I ended up defining a class for the
Result
elements, where it explicitly checks whether each
Result
element should re-render based on whether it was previously selected and based on the current user input :

var ResultItem = React.createClass({
shouldComponentUpdate : function(nextProps) {
if (nextProps.match !== this.props.match) {
return true;
} else {
return (nextProps.selected !== this.props.selected);
}
},
render : function() {
return (
<li onClick={this.props.handleListClick}
data-path={this.props.file}
className={
(this.props.selected) ? "valid selected" : "valid"
}
key={this.props.file} >
{this.props.children}
</li>
);
}
});


And the list is now created as such:

results = this.state.filtered.map(function(file, index) {
var start, end, matchIndex, match = this.state.query, selected;

matchIndex = file.indexOf(match);
start = file.slice(0, matchIndex);
end = file.slice(matchIndex + match.length);
selected = (index === this.state.currentlySelected) ? true : false

return (
<ResultItem handleClick={this.handleListClick}
data-path={file}
selected={selected}
key={file}
match={match} >
{start}
<span className="marked">{match}</span>
{end}
</ResultItem>
);
}.bind(this));
}


This made performance slightly better, but it's still not good enough. Thing is when I tested on the production version of React things worked buttery smooth, no lag at all.

BOTTOMLINE

Is such a noticeable discrepancy between development and production versions of React normal?

Am I understanding/doing something wrong when I think about how React manages the list?

Answer

As with many of the other answers to this question the main problem lies in the fact that rendering so many elements in the DOM whilst doing filtering and handling key events is going to be slow.

You are not doing anything inherently wrong with regards to React that is causing the issue but like many of the issues that are performance related the UI can also take a big percentage of the blame.

If your UI is not designed with efficiency in mind even tools like React that are designed to be performant will suffer.

Filtering the result set is a great start as mentioned by @Koen

I've played around with the idea a bit and created an example app illustrating how I might start to tackle this kind of problem.

This is by no means production ready code but it does illustrate the concept adequately and can be modified to be more robust, feel free to take a look at the code - I hope at the very least it gives you some ideas...;)

https://github.com/deowk/react-large-list-example

enter image description here

Comments