I am in the process of implementing a filterable list with React. The structure of the list is as shown in the image below.
PREMISE
Here's a description of how it is supposed to work:
Search
component.{ 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 forcurrentlySelectedIndex
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>
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-renderPROBLEM
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?
UPDATE 14-11-2016
I have found this presentation of Michael Jackson, where he tackles an issue very similar to this one: https://youtu.be/7S8v8jfLb1Q?t=26m2s
The solution is very similar to the one proposed by AskarovBeknar's answer, below
UPDATE 14-4-2018
Since this is apparently a popular question and things have progressed since the original question was asked, while I do encourage you to watch the video linked above, in order to get a grasp of a virtual layout, I also encourage you to use the React Virtualized library if you do not want to re-invent the wheel.
When handling a large list, it's important not to render all the data at once to avoid overloading the DOM tree. The best approach to improving performance depends on your use case. If you prefer to render all the data in one place, infinite scroll or a windowing technique would be your best bet.
To optimize React rendering, you need to make sure that components receive only necessary props. It will let you control the CPU consumption and avoid over-rendering unnecessary features. The solution is to create a functional component that will collect all props and redistribute them to other components.
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...;)
react-large-list-example
My experience with a very similar problem is that react really suffers if there are more than 100-200 or so components in the DOM at once. Even if you are super careful (by setting up all your keys and/or implementing a shouldComponentUpdate
method) to only to change one or two components on a re-render, you're still going to be in a world of hurt.
The slow part of react at the moment is when it compares the difference between the virtual DOM and the real DOM. If you have thousands of components but only update a couple, it doesn't matter, react still has a massive difference operation to do between the DOMs.
When I write pages now I try to design them to minimise the number of components, one way to do this when rendering large lists of components is to... well... not render large lists of components.
What I mean is: only render the components you can currently see, render more as you scroll down, you're user isn't likely to scroll down through thousands of components any way.... I hope.
A great library for doing this is:
https://www.npmjs.com/package/react-infinite-scroll
With a great how-to here:
http://www.reactexamples.com/react-infinite-scroll/
I'm afraid it doesn't remove components that are off the top of the page though, so if you scroll for long enough you're performance issues will start to reemerge.
I know it isn't good practice to provide a link as answer, but the examples they provide are going to explain how to use this library much better than I can here. Hopefully I have explained why big lists are bad, but also a work around.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With