Jestem w trakcie wdrażania listy z możliwością filtrowania w React. Struktura listy jest taka, jak pokazano na poniższej ilustracji.
PRZESŁANKA
Oto opis tego, jak to powinno działać:
- Stan znajduje się w komponencie najwyższego poziomu,
Search
komponencie. - Stan jest opisany następująco:
{ widoczne: boolean, pliki: tablica, filtrowane: tablica, zapytanie: ciąg, currentSelectedIndex: integer }
files
to potencjalnie bardzo duża tablica zawierająca ścieżki do plików (10 000 wpisów to wiarygodna liczba).filtered
to przefiltrowana tablica po wpisaniu przez użytkownika co najmniej 2 znaków. Wiem, że to dane pochodne i jako taki można by argumentować o przechowywaniu ich w stanie, ale jest to potrzebnecurrentlySelectedIndex
który jest indeksem aktualnie wybranego elementu z przefiltrowanej listy.Użytkownik wpisuje więcej niż 2 litery do
Input
komponentu, tablica jest filtrowana i dla każdego wpisu w filtrowanej tablicyResult
jest renderowany komponentKażdy
Result
składnik wyświetla pełną ścieżkę, która częściowo pasuje do zapytania, a część ścieżki częściowego dopasowania jest podświetlona. Na przykład DOM komponentu wynikowego, gdyby użytkownik wpisał „le”, wyglądałby tak:<li>this/is/a/fi<strong>le</strong>/path</li>
- Jeśli użytkownik naciśnie klawisze w górę lub w dół, gdy
Input
składnik jest aktywny,currentlySelectedIndex
zmiany oparte nafiltered
tablicy. Powoduje toResult
, że komponent zgodny z indeksem zostanie oznaczony jako wybrany, co spowoduje ponowne renderowanie
PROBLEM
Początkowo testowałem to z wystarczająco małą tablicą files
, używając rozwojowej wersji Reacta i wszystko działało dobrze.
Problem pojawił się, gdy miałem do czynienia z files
tablicą liczącą aż 10000 wpisów. Wpisanie 2 liter na wejściu wygenerowałoby dużą listę, a kiedy naciskałem klawisze w górę iw dół, aby się po niej poruszać, byłoby bardzo opóźnione.
Na początku nie miałem zdefiniowanego komponentu dla Result
elementów i robiłem po prostu listę w locie, przy każdym renderowaniu Search
komponentu, jako takiej:
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));
Jak widać, za każdym razem, gdy currentlySelectedIndex
zmiana powodowała ponowne renderowanie, a lista byłaby odtwarzana za każdym razem. Pomyślałem, że skoro ustawiłem key
wartość dla każdego li
elementu, React uniknie ponownego renderowania każdego innego li
elementu, który nie miał swojej className
zmiany, ale najwyraźniej tak nie było.
Skończyło się na tym Result
, że zdefiniowałem klasę dla elementów, w której wyraźnie sprawdza, czy każdy Result
element powinien być ponownie renderowany na podstawie tego, czy został wcześniej wybrany i na podstawie aktualnych danych wejściowych użytkownika:
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>
);
}
});
A lista jest teraz tworzona jako taka:
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));
}
To sprawiło, że wydajność jest nieco lepsza, ale nadal nie jest wystarczająco dobra. Rzecz w tym, że testowałem na produkcyjnej wersji Reacta, wszystko działało gładko, bez żadnych opóźnień.
DOLNA LINIA
Czy taka zauważalna rozbieżność między wersjami deweloperskimi i produkcyjnymi Reacta jest normalna?
Czy rozumiem / robię coś złego, gdy myślę o tym, jak React zarządza listą?
AKTUALIZACJA 14-11-2016
Znalazłem tę prezentację Michaela Jacksona, w której podejmuje on problem bardzo podobny do tego: https://youtu.be/7S8v8jfLb1Q?t=26m2s
Rozwiązanie jest bardzo podobne do rozwiązania zaproponowanego w odpowiedzi AskarovBeknara poniżej
AKTUALIZACJA 14-4-2018
Ponieważ jest to najwyraźniej popularne pytanie, a od czasu zadania oryginalnego pytania sytuacja się rozwinęła, a zachęcam do obejrzenia wideo, do którego link znajduje się powyżej, w celu zapoznania się z wirtualnym układem, zachęcam również do korzystania z narzędzia React Virtualized biblioteka, jeśli nie chcesz wymyślać koła na nowo.
źródło
Odpowiedzi:
Podobnie jak w przypadku wielu innych odpowiedzi na to pytanie, główny problem polega na tym, że renderowanie tak wielu elementów w DOM podczas filtrowania i obsługi kluczowych zdarzeń będzie przebiegać wolno.
Nie robisz niczego z natury złego w związku z Reactem, który powoduje problem, ale podobnie jak wiele innych problemów związanych z wydajnością, interfejs użytkownika może również ponosić duży procent winy.
Jeśli Twój interfejs użytkownika nie jest zaprojektowany z myślą o wydajności, ucierpią nawet narzędzia takie jak React, które zostały zaprojektowane z myślą o wydajności.
Filtrowanie zestawu wyników to świetny początek, o czym wspomniał @Koen
Bawiłem się trochę tym pomysłem i stworzyłem przykładową aplikację, która ilustruje, jak mogę zacząć rozwiązywać tego rodzaju problemy.
Nie jest to bynajmniej
production ready
kod, ale dobrze ilustruje koncepcję i można go zmodyfikować, aby był bardziej solidny, zachęcamy do obejrzenia kodu - mam nadzieję, że przynajmniej daje kilka pomysłów ...;)przykład-dużej-listy-reakcji
źródło
127.0.0.1 * http://localhost:3001
?Moje doświadczenie z bardzo podobnym problemem jest takie, że reakcja naprawdę cierpi, jeśli w DOM jest więcej niż 100-200 lub więcej komponentów na raz. Nawet jeśli jesteś bardzo ostrożny (ustawiając wszystkie klucze i / lub wdrażając
shouldComponentUpdate
metodę), aby zmienić tylko jeden lub dwa komponenty podczas ponownego renderowania, nadal będziesz w świecie zranienia.Powolną częścią reakcji w tej chwili jest porównanie różnicy między wirtualnym DOM a rzeczywistym DOM. Jeśli masz tysiące komponentów, ale aktualizujesz tylko kilka, nie ma to znaczenia, reakcja wciąż ma ogromną różnicę do wykonania między modelami DOM.
Kiedy teraz piszę strony, staram się projektować je tak, aby zminimalizować liczbę komponentów, jednym ze sposobów na zrobienie tego podczas renderowania dużych list komponentów jest ... cóż ... nie renderowanie dużych list komponentów.
Chodzi mi o to, że: renderuj tylko komponenty, które obecnie widzisz, renderuj więcej podczas przewijania w dół, prawdopodobnie użytkownik nie przewinie w żaden sposób tysięcy komponentów ... Mam nadzieję.
Świetną biblioteką do tego jest:
https://www.npmjs.com/package/react-infinite-scroll
Ze świetnym poradnikiem:
http://www.reactexamples.com/react-infinite-scroll/
Obawiam się, że nie usuwa on jednak komponentów, które znajdują się na górze strony, więc jeśli przewijasz wystarczająco długo, problemy z wydajnością zaczną się ponownie pojawiać.
Wiem, że podanie linku jako odpowiedzi nie jest dobrą praktyką, ale podane przez nich przykłady wyjaśnią, jak korzystać z tej biblioteki znacznie lepiej niż ja tutaj. Mam nadzieję, że wyjaśniłem, dlaczego duże listy są złe, ale także obejść.
źródło
Po pierwsze, różnica między wersją deweloperską i produkcyjną Reacta jest ogromna, ponieważ w produkcji jest wiele pomijanych testów poprawności (takich jak weryfikacja typów rekwizytów).
Wtedy myślę, że powinieneś ponownie rozważyć użycie Redux, ponieważ byłoby to niezwykle pomocne w tym, czego potrzebujesz (lub jakiejkolwiek implementacji strumienia). Powinieneś definitywnie przyjrzeć się tej prezentacji: Big List High Performance React & Redux .
Ale zanim zagłębimy się w redux, musisz dokonać pewnych poprawek w swoim kodzie React, dzieląc komponenty na mniejsze komponenty, ponieważ
shouldComponentUpdate
całkowicie ominie renderowanie dzieci, więc jest to ogromny zysk .Gdy masz bardziej szczegółowe komponenty, możesz obsłużyć stan za pomocą redux i re-redux, aby lepiej zorganizować przepływ danych.
Niedawno miałem podobny problem, gdy musiałem wyrenderować tysiąc wierszy i móc modyfikować każdy wiersz, edytując jego zawartość. Ta miniaplikacja wyświetla listę koncertów z potencjalnymi duplikatami koncertów i muszę wybrać dla każdego potencjalnego duplikatu, jeśli chcę oznaczyć potencjalny duplikat jako oryginalny koncert (nie duplikat), zaznaczając pole wyboru i, jeśli to konieczne, edytować nazwa koncertu. Jeśli nic nie zrobię dla konkretnego potencjalnego duplikatu, zostanie on uznany za zduplikowany i zostanie usunięty.
Oto jak to wygląda:
Istnieją zasadniczo 4 komponenty sieci (jest tutaj tylko jeden rząd, ale to dla przykładu):
Oto pełny kod (CodePen pracy: Ogromne Lista z React & Redux ) używając Redux , reagują-Redux , niezmienne , Reselect i skomponuj :
const initialState = Immutable.fromJS({ /* See codepen, this is a HUGE list */ }) const types = { CONCERTS_DEDUP_NAME_CHANGED: 'diggger/concertsDeduplication/CONCERTS_DEDUP_NAME_CHANGED', CONCERTS_DEDUP_CONCERT_TOGGLED: 'diggger/concertsDeduplication/CONCERTS_DEDUP_CONCERT_TOGGLED', }; const changeName = (pk, name) => ({ type: types.CONCERTS_DEDUP_NAME_CHANGED, pk, name }); const toggleConcert = (pk, toggled) => ({ type: types.CONCERTS_DEDUP_CONCERT_TOGGLED, pk, toggled }); const reducer = (state = initialState, action = {}) => { switch (action.type) { case types.CONCERTS_DEDUP_NAME_CHANGED: return state .updateIn(['names', String(action.pk)], () => action.name) .set('_state', 'not_saved'); case types.CONCERTS_DEDUP_CONCERT_TOGGLED: return state .updateIn(['concerts', String(action.pk)], () => action.toggled) .set('_state', 'not_saved'); default: return state; } }; /* configureStore */ const store = Redux.createStore( reducer, initialState ); /* SELECTORS */ const getDuplicatesGroups = (state) => state.get('duplicatesGroups'); const getDuplicateGroup = (state, name) => state.getIn(['duplicatesGroups', name]); const getConcerts = (state) => state.get('concerts'); const getNames = (state) => state.get('names'); const getConcertName = (state, pk) => getNames(state).get(String(pk)); const isConcertOriginal = (state, pk) => getConcerts(state).get(String(pk)); const getGroupNames = reselect.createSelector( getDuplicatesGroups, (duplicates) => duplicates.flip().toList() ); const makeGetConcertName = () => reselect.createSelector( getConcertName, (name) => name ); const makeIsConcertOriginal = () => reselect.createSelector( isConcertOriginal, (original) => original ); const makeGetDuplicateGroup = () => reselect.createSelector( getDuplicateGroup, (duplicates) => duplicates ); /* COMPONENTS */ const DuplicatessTableRow = Recompose.onlyUpdateForKeys(['name'])(({ name }) => { return ( <tr> <td>{name}</td> <DuplicatesRowColumn name={name}/> </tr> ) }); const PureToggle = Recompose.onlyUpdateForKeys(['toggled'])(({ toggled, ...otherProps }) => ( <input type="checkbox" defaultChecked={toggled} {...otherProps}/> )); /* CONTAINERS */ let DuplicatesTable = ({ groups }) => { return ( <div> <table className="pure-table pure-table-bordered"> <thead> <tr> <th>{'Concert'}</th> <th>{'Duplicates'}</th> </tr> </thead> <tbody> {groups.map(name => ( <DuplicatesTableRow key={name} name={name} /> ))} </tbody> </table> </div> ) }; DuplicatesTable.propTypes = { groups: React.PropTypes.instanceOf(Immutable.List), }; DuplicatesTable = ReactRedux.connect( (state) => ({ groups: getGroupNames(state), }) )(DuplicatesTable); let DuplicatesRowColumn = ({ duplicates }) => ( <td> <ul> {duplicates.map(d => ( <DuplicateItem key={d} pk={d}/> ))} </ul> </td> ); DuplicatessRowColumn.propTypes = { duplicates: React.PropTypes.arrayOf( React.PropTypes.string ) }; const makeMapStateToProps1 = (_, { name }) => { const getDuplicateGroup = makeGetDuplicateGroup(); return (state) => ({ duplicates: getDuplicateGroup(state, name) }); }; DuplicatesRowColumn = ReactRedux.connect(makeMapStateToProps1)(DuplicatesRowColumn); let DuplicateItem = ({ pk, name, toggled, onToggle, onNameChange }) => { return ( <li> <table> <tbody> <tr> <td>{ toggled ? <input type="text" value={name} onChange={(e) => onNameChange(pk, e.target.value)}/> : name }</td> <td> <PureToggle toggled={toggled} onChange={(e) => onToggle(pk, e.target.checked)}/> </td> </tr> </tbody> </table> </li> ) } const makeMapStateToProps2 = (_, { pk }) => { const getConcertName = makeGetConcertName(); const isConcertOriginal = makeIsConcertOriginal(); return (state) => ({ name: getConcertName(state, pk), toggled: isConcertOriginal(state, pk) }); }; DuplicateItem = ReactRedux.connect( makeMapStateToProps2, (dispatch) => ({ onNameChange(pk, name) { dispatch(changeName(pk, name)); }, onToggle(pk, toggled) { dispatch(toggleConcert(pk, toggled)); } }) )(DuplicateItem); const App = () => ( <div style={{ maxWidth: '1200px', margin: 'auto' }}> <DuplicatesTable /> </div> ) ReactDOM.render( <ReactRedux.Provider store={store}> <App/> </ReactRedux.Provider>, document.getElementById('app') );
Wnioski wyciągnięte z wykonania tej miniaplikacji podczas pracy z ogromnym zbiorem danych
connect
komponent ed dla komponentu, który jest najbliższy z potrzebnych danych, aby uniknąć przekazywania komponentu tylko właściwości, których nie używająownProps
jest konieczne, aby uniknąć niepotrzebnego ponownego renderowaniaźródło
Reakcja w wersji rozwojowej sprawdza właściwości każdego komponentu, aby ułatwić proces rozwoju, podczas gdy w produkcji jest on pomijany.
Filtrowanie listy ciągów jest bardzo kosztowną operacją dla każdego klucza. Może to powodować problemy z wydajnością z powodu jednowątkowego charakteru JavaScript. Rozwiązaniem może być użycie metody debounce , aby opóźnić wykonanie funkcji filtru do momentu wygaśnięcia opóźnienia.
Innym problemem może być sama ogromna lista. Możesz stworzyć wirtualny układ i ponownie wykorzystać utworzone elementy, zastępując dane. Zasadniczo tworzysz przewijalny komponent kontenera o stałej wysokości, wewnątrz którego umieścisz kontener z listą. Wysokość kontenera listy należy ustawić ręcznie (itemHeight * numberOfItems) w zależności od długości widocznej listy, aby pasek przewijania działał. Następnie utwórz kilka komponentów przedmiotów, tak aby wypełniały wysokość przewijalnych kontenerów i być może dodają dodatkowy jeden lub dwa naśladujące ciągły efekt listy. ustaw je w pozycji absolutnej, a po przewinięciu po prostu przesuń ich pozycję, aby naśladowała ciągłą listę (myślę, że dowiesz się, jak to zaimplementować :)
Jeszcze jedno, że pisanie do DOM jest kosztowną operacją, zwłaszcza jeśli zrobisz to źle. Możesz używać kanwy do wyświetlania list i płynnie obsługiwać przewijanie. Komponenty React-Canvas w usłudze Checkout. Słyszałem, że wykonali już trochę pracy nad Listami.
źródło
React in development
? i po co sprawdzać prototypy każdego komponentu?Sprawdź React Virtualized Select, został zaprojektowany, aby rozwiązać ten problem i działa imponująco w moim odczuciu. Z opisu:
https://github.com/bvaughn/react-virtualized-select
źródło
Jak wspomniałem w moim komentarzu , wątpię, aby użytkownicy potrzebowali tych wszystkich 10000 wyników naraz w przeglądarce.
Co się stanie, jeśli przejrzysz wyniki i zawsze wyświetlasz listę 10 wyników.
Mam stworzony przykład przy użyciu tej techniki, bez pomocy innych bibliotek, takich jak Redux. Obecnie tylko z nawigacją za pomocą klawiatury, ale można go łatwo rozszerzyć również o pracę przy przewijaniu.
Przykład składa się z 3 komponentów: aplikacji kontenera, komponentu wyszukiwania i komponentu listy. Prawie cała logika została przeniesiona do komponentu kontenera.
Kłamstwa GIST w zachowaniu ścieżki
start
iselected
rezultatu, a przeniesienie tych interakcji klawiatury.nextResult: function() { var selected = this.state.selected + 1 var start = this.state.start if(selected >= start + this.props.limit) { ++start } if(selected + start < this.state.results.length) { this.setState({selected: selected, start: start}) } }, prevResult: function() { var selected = this.state.selected - 1 var start = this.state.start if(selected < start) { --start } if(selected + start >= 0) { this.setState({selected: selected, start: start}) } },
Po prostu przepuszczając wszystkie pliki przez filtr:
updateResults: function() { var results = this.props.files.filter(function(file){ return file.file.indexOf(this.state.query) > -1 }, this) this.setState({ results: results }); },
I krojenie wyników na podstawie
start
ilimit
wrender
metodzie:render: function() { var files = this.state.results.slice(this.state.start, this.state.start + this.props.limit) return ( <div> <Search onSearch={this.onSearch} onKeyDown={this.onKeyDown} /> <List files={files} selected={this.state.selected - this.state.start} /> </div> ) }
Skrzypce zawierające pełny działający przykład: https://jsfiddle.net/koenpunt/hm1xnpqk/
źródło
Wypróbuj filtr przed załadowaniem do komponentu React i pokaż tylko rozsądną liczbę elementów w komponencie i załaduj więcej na żądanie. Nikt nie może wyświetlić jednocześnie tylu elementów.
Myślę, że tak nie jest, ale nie używaj indeksów jako kluczy .
Aby poznać prawdziwy powód, dla którego wersje rozwojowe i produkcyjne są różne, możesz wypróbować
profiling
swój kod.Załaduj swoją stronę, rozpocznij nagrywanie, dokonaj zmiany, zatrzymaj nagrywanie, a następnie sprawdź chronometraż. Zobacz tutaj, aby uzyskać instrukcje dotyczące profilowania wydajności w przeglądarce Chrome .
źródło
Dla każdego borykającego się z tym problemem napisałem komponent
react-big-list
który obsługuje listy do 1 miliona rekordów.Oprócz tego ma kilka ciekawych dodatkowych funkcji, takich jak:
Używamy go w produkcji w kilku aplikacjach i działa świetnie.
źródło
React ma
react-window
bibliotekę rekomendacji : https://www.npmjs.com/package/react-windowLepiej niż
react-vitualized
. Możesz tego spróbowaćźródło