2016-06-25 22 views
28

Sono in procinto di implementare un elenco filtrabile con React. La struttura della lista è come mostrato nell'immagine qui sotto.Elenco grandi prestazioni con React

enter image description here

PREMESSA

Ecco una descrizione di come dovrebbe funzionare:

  • Lo stato risiede nel componente più alto livello, il componente Search.
  • Lo stato è descritto come segue:
 
{ 
    visible : boolean, 
    files : array, 
    filtered : array, 
    query : string, 
    currentlySelectedIndex : integer 
} 
  • files è una potenzialmente molto grandi percorsi di file, matrice contenente (10000 voci è un numero plausibile).
  • filtered è l'array filtrato dopo che l'utente ha digitato almeno 2 caratteri. So che si tratta di dati derivati ​​e come tale potrebbe essere fatto per archiviarlo nello stato ma è necessario per
  • currentlySelectedIndex che è l'indice dell'elemento attualmente selezionato dall'elenco filtrato.

  • tipi più utenti 2 lettere nel componente Input, la matrice viene filtrato e per ogni voce nella matrice filtrato un componente Result è resa

  • Ogni componente Result sta visualizzando il percorso completo che parzialmente abbinato il query, e la parte di corrispondenza parziale del percorso è evidenziata. Per esempio il DOM di un componente Risultato, se l'utente avesse digitato 'le' sarebbe qualcosa di simile a questo:

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

  • Se l'utente preme i tasti su o giù mentre la componente Input è focalizzato le currentlySelectedIndex modifiche basato sull'array filtered. In questo modo il componente Result che corrisponde all'indice da marcare come selezionato causando un re-rendering

PROBLEMA

Inizialmente ho provato questo con una piccola gamma sufficiente di files, utilizzando la versione di sviluppo di React e tutto ha funzionato bene

Il problema si presentava quando avevo a che fare con un array files grande come 10000 voci. Digitando 2 lettere nell'input si generava una grande lista e quando si premevano i tasti su e giù per spostarsi sarebbe molto lenta.

In un primo momento non ha avuto un componente definito per i Result elementi e Stavo semplicemente facendo la lista al volo, su ogni rendering del componente Search, come ad esempio:

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)); 

Come si può dire ogni volta che lo currentlySelectedIndex cambiava, causava un nuovo rendering e l'elenco veniva ricreato ogni volta. Ho pensato che dal momento che avevo impostato un valore key su ogni elemento li, React evitava di ri-rendering ogni altro elemento li che non aveva la sua modifica className, ma a quanto pare non era così.

ho finito per la definizione di una classe per gli Result elementi, dove controlla se esplicitamente se ogni elemento Result dovrebbe ri-renderizzare in base al fatto che è stato precedentemente selezionato e basato sul input dell'utente corrente:

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> 
     ); 
    } 
}); 

E l'elenco è stato creato come tale:

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)); 
} 

Ciò ha reso le prestazioni leggermente meglio, ma non è ancora abbastanza buono. Il fatto è che quando ho provato la versione di produzione di React, le cose hanno funzionato bene, senza problemi.

bottomline

è una discrepanza tale evidente tra le versioni di sviluppo e produzione di Reagire normale?

Sono in grado di capire/fare qualcosa di sbagliato quando penso a come React gestisce l'elenco?

UPDATE 14-11-2016

ho trovato questa presentazione di Michael Jackson, in cui egli affronta un problema molto simile a questo: https://youtu.be/7S8v8jfLb1Q?t=26m2s

La soluzione è molto simile a quella proposto da AskarovBeknar's answer, sotto

+0

Che cosa si intende per sviluppo/versione di produzione di reagire? – Dibesjr

+0

@Dibesjr https://facebook.github.io/react/downloads.html#development-vs.-production-builds –

+0

Ah capisco, grazie. Quindi, per rispondere a una delle tue domande, dice che c'è una discrepanza nell'ottimizzazione tra le versioni.Una cosa a cui prestare attenzione nelle grandi liste è la creazione di funzioni nel rendering. Avrà un impatto sulle prestazioni quando entrerai in una lista gigante. Vorrei provare a vedere quanto tempo ci vuole per generare quell'elenco usando i loro strumenti perf https://facebook.github.io/react/docs/perf.html – Dibesjr

risposta

5

Come molte altre risposte a questa domanda, il problema principale sta nel fatto che il rendering di così tanti elementi nel DOM mentre si eseguono il filtraggio e la gestione degli eventi chiave sarà lento.

Non si sta facendo nulla di intrinsecamente sbagliato riguardo a React che sta causando il problema, ma come molti dei problemi correlati alle prestazioni, l'interfaccia utente può anche prendere una grande percentuale della colpa.

Se l'interfaccia utente non è progettata pensando all'efficienza, anche gli strumenti come React progettati per essere performanti ne risentiranno.

Filtrare il set di risultati è un ottimo inizio come detto da @Koen

Ho giocato intorno con l'idea un po 'e ha creato un app esempio che illustra come potrei iniziare ad affrontare questo tipo di problema.

Questo non è affatto il codice production ready ma illustra adeguatamente il concetto e può essere modificato per essere più robusto, sentiti libero di dare un'occhiata al codice - spero che almeno ti dia qualche idea .. .;)

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

enter image description here

+0

Mi sento davvero male per dover scegliere una sola risposta, sembrano tutti essersi sforzati, ma attualmente sono in vacanza senza PC e non riesco a controllarli con l'attenzione che meritano . Ho scelto questo perché è abbastanza corto e al punto da capire anche quando si legge da un telefono. La ragione non lo so. –

+0

Cosa intendi per modifica file host '127.0.0.1 * http: // localhost: 3001'? – stackjlei

4

Prima di tutto, la differenza tra lo sviluppo e la versione di produzione di React è enorme perché nella produzione ci sono molti controlli di integrità saltati (come i tipi di puntelli verifica).

Quindi, penso che dovresti riconsiderare l'uso di Redux perché sarebbe estremamente utile qui per ciò di cui hai bisogno (o qualsiasi tipo di implementazione del flusso). Deve dare un'occhiata definitiva a questa presentazione: Big List High Performance React & Redux.

Ma prima di immergersi in redux, è necessario apportato alcune ajustements al codice Reagire suddividendo i componenti in componenti più piccoli, perché shouldComponentUpdate sarà completamente bypassare il rendering dei bambini, quindi è un enorme guadagno.

Quando si dispone di più componenti granulari, è possibile gestire lo stato con redux e react-redux per organizzare meglio il flusso di dati.

Recentemente ho dovuto affrontare un problema simile quando avevo bisogno di eseguire il rendering di mille righe ed essere in grado di modificare ogni riga modificandone il contenuto. Questa mini app visualizza un elenco di concerti con potenziali duplicati di concerti e ho bisogno di scegliere per ogni potenziale duplicato se voglio contrassegnare il potenziale duplicato come un concerto originale (non un duplicato) selezionando la casella di controllo e, se necessario, modifica il nome del concerto. Se non faccio nulla per un particolare potenziale elemento duplicato, sarà considerato duplicato e verrà eliminato.

Ecco come appare:

enter image description here

Ci sono fondamentalmente 4 componenti principali (v'è una sola fila qui ma è per il bene dell'esempio):

enter image description here

Ecco il codice completo (codice funzionante: Huge List with React & Redux) utilizzando redux, react-redux, immutable, reselect e recompose:

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') 
); 

Le lezioni apprese in questo modo mini applicazione quando si lavora con enormi set di dati

  • Reagire componenti funzionano meglio quando sono mantenute piccole
  • Riseleziona diventare molto utile per evitare ricalcolo e mantenere lo stesso oggetto di riferimento (quando si utilizza immutable.js) con gli stessi argomenti.
  • Crea connect componente ed per il componente che sono i più vicini dei dati di cui hanno bisogno per evitare di avere componente solo di passaggio verso il basso oggetti di scena che non usano
  • uso della funzione di tessuto per creare mapDispatchToProps quando è necessario solo il l'oggetto prop iniziale in ownProps è necessario per evitare inutili rivisitazioni
  • Reagire & ride definitivamente rock insieme!
+0

Non credo che l'aggiunta di una dipendenza da ridire sia necessaria per risolvere il problema dell'OP, ulteriori azioni di dispacciamento per filtrare il suo set di risultati potrebbero solo aggravare il problema, i dispacci non sono così poco costosi come si potrebbe pensare, gestendo questa particolare situazione con il locale lo stato dei componenti è l'approccio più efficiente – deowk

7

La mia esperienza con un problema molto simile è che la reazione soffre davvero se ci sono più di 100-200 componenti nel DOM in una sola volta. Anche se sei molto attento (impostando tutte le chiavi e/o implementando un metodo shouldComponentUpdate) solo per cambiare uno o due componenti su un re-rendering, sarai comunque in un mondo di dolore.

La parte lenta di reagire al momento è quando confronta la differenza tra il DOM virtuale e il DOM reale. Se hai migliaia di componenti ma aggiorni solo un paio, non importa, reagire ha ancora una grande differenza tra le operazioni da fare tra i DOM.

Quando scrivo pagine ora cerco di progettarle per ridurre al minimo il numero di componenti, un modo per eseguire questa operazione quando il rendering di elenchi di componenti di grandi dimensioni è ... beh ... non visualizzare elenchi di componenti di grandi dimensioni.

Quello che voglio dire è: solo rendere i componenti che puoi vedere attualmente, renderne di più mentre scorri verso il basso, l'utente non è in grado di scorrere verso il basso attraverso migliaia di componenti in qualsiasi modo .... Spero.

una grande biblioteca per fare questo è:

https://www.npmjs.com/package/react-infinite-scroll

Con una grande how-to qui:

http://www.reactexamples.com/react-infinite-scroll/

Temo che non rimuove componenti che sono in cima alla pagina, quindi se si scorre per un tempo sufficientemente lungo, i problemi di prestazioni inizieranno a riemergere.

So che non è una buona pratica fornire un collegamento come risposta, ma gli esempi che forniscono spiegheranno come usare questa libreria molto meglio di quanto possa fare qui. Spero di aver spiegato perché le grandi liste sono cattive, ma anche una soluzione.

1
  1. Reagire nei controlli di versione di sviluppo per proptypes di ciascun componente per facilitare il processo di sviluppo, mentre nella produzione viene omesso.

  2. L'elenco di filtri delle stringhe è un'operazione molto costosa per ogni keyup. potrebbe causare problemi di prestazioni a causa della natura a thread singolo di JavaScript. La soluzione potrebbe essere utilizzare il metodo antirimbalzo per ritardare l'esecuzione della funzione del filtro fino a quando il ritardo non è scaduto.

  3. Un altro problema potrebbe essere l'enorme elenco stesso. È possibile creare il layout virtuale e riutilizzare gli elementi creati sostituendo semplicemente i dati. Fondamentalmente si crea un componente contenitore scorrevole con altezza fissa, all'interno del quale si posizionerà il contenitore dell'elenco. L'altezza del contenitore dell'elenco deve essere impostata manualmente (itemHeight * numberOfItems) in base alla lunghezza dell'elenco visibile, per far funzionare una barra di scorrimento. Quindi crea alcuni componenti dell'articolo in modo da riempire l'altezza dei contenitori scorrevoli e magari aggiungere uno o due ulteriori effetti di lista continua. rendi loro la posizione assoluta e su scroll sposta semplicemente la loro posizione in modo che imiterà la lista continua (penso che scoprirai come implementarla :)

  4. Un'altra cosa è scrivere in DOM è anche un'operazione costosa specialmente se tu fallo male È possibile utilizzare la tela per visualizzare elenchi e creare un'esperienza fluida sullo scorrimento. Esegui il check-up dei componenti del canvas. Ho sentito che hanno già lavorato sulle liste.

1

Prova il filtro prima di caricarlo nel componente React e mostra solo una quantità ragionevole di articoli nel componente e carica più a richiesta. Nessuno può visualizzare tanti articoli contemporaneamente.

I don't think you are, but don't use indexes as keys.

Per scoprire il vero motivo per cui le versioni di sviluppo e produzione sono diverse, è possibile provare il codice profiling.

Caricare la pagina, avviare la registrazione, eseguire una modifica, interrompere la registrazione e quindi controllare i tempi. Vedi here for instructions for performance profiling in Chrome.

2

Come ho già detto in my comment, dubito che gli utenti debbano tutti quei 10000 risultati nel browser in una volta.

Cosa succede se si sfoglia i risultati e si mostra sempre un elenco di 10 risultati.

Ho created an example utilizzando questa tecnica, senza utilizzare alcuna altra libreria come Redux. Attualmente solo con la navigazione da tastiera, ma potrebbe essere facilmente esteso per funzionare anche sullo scorrimento.

L'esempio esiste di 3 componenti, l'applicazione contenitore, un componente di ricerca e un componente di elenco. Quasi tutta la logica è stata spostata nel componente contenitore.

L'essenza sta nel tenere traccia dello start e del risultato di selected e di spostarli nell'interazione della tastiera.

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}) 
    } 
}, 

Mentre semplicemente passando tutti i file attraverso un filtro:

updateResults: function() { 
    var results = this.props.files.filter(function(file){ 
    return file.file.indexOf(this.state.query) > -1 
    }, this) 

    this.setState({ 
    results: results 
    }); 
}, 

E affettare i risultati in base start e limit nel metodo render:

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> 
) 
} 

Fiddle contenente un lavoro completo esempio: https://jsfiddle.net/koenpunt/69z2wepo/47841/

1

check out Reagire virtualizzato selezionare, è progettato per risolvere questo problema ed esegue impressionante nella mia esperienza. Dalla descrizione:

HOC che utilizza reagire virtualizzato e reagire di selezione per visualizzare grandi liste di opzioni in un menu a discesa

https://github.com/bvaughn/react-virtualized-select

Problemi correlati