Grandi prestazioni da elenco con React


86

Sto implementando un elenco filtrabile con React. La struttura dell'elenco è come mostrato nell'immagine sottostante.

inserisci qui la descrizione dell'immagine

PREMESSA

Ecco una descrizione di come dovrebbe funzionare:

  • Lo stato risiede nella componente di livello più alto, la Searchcomponente.
  • Lo stato è descritto come segue:
{
    visibile: booleano,
    file: array,
    filtrato: array,
    stringa della domanda,
    currentlySelectedIndex: intero
}
  • files è un array potenzialmente molto grande, contenente percorsi di file (10000 voci è un numero plausibile).
  • filteredè l'array filtrato dopo che l'utente ha digitato almeno 2 caratteri. So che sono dati derivati ​​e come tale si potrebbe argomentare sulla memorizzazione nello stato, ma è necessario per
  • currentlySelectedIndex che è l'indice dell'elemento attualmente selezionato dall'elenco filtrato.

  • L'utente digita più di 2 lettere nel Inputcomponente, l'array viene filtrato e per ogni voce nell'array filtrato Resultviene visualizzato un componente

  • Ogni Resultcomponente visualizza il percorso completo che corrisponde parzialmente alla query e la parte di corrispondenza parziale del percorso viene evidenziata. Ad esempio, il DOM di un componente Risultato, se l'utente avesse digitato "le" sarebbe qualcosa del genere:

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

  • Se l'utente preme i tasti su o giù mentre il Inputcomponente è focalizzato, le currentlySelectedIndexmodifiche si basano filteredsull'array. Ciò fa sì che il Resultcomponente che corrisponde all'indice venga contrassegnato come selezionato provocando un nuovo rendering

PROBLEMA

Inizialmente l'ho testato con un array abbastanza piccolo di files, utilizzando la versione di sviluppo di React, e tutto ha funzionato bene.

Il problema è apparso quando ho dovuto gestire un filesarray grande quanto 10000 voci. Digitare 2 lettere nell'Input genererebbe un grande elenco e quando premevo i tasti su e giù per spostarlo sarebbe stato molto lento.

All'inizio non avevo un componente definito per gli Resultelementi e stavo semplicemente facendo l'elenco al volo, su ogni render del Searchcomponente, in quanto tale:

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 puoi vedere, ogni volta che la currentlySelectedIndexmodifica viene eseguita, viene eseguito un nuovo rendering e l'elenco viene ricreato ogni volta. Pensavo che poiché avevo impostato un keyvalore su ogni lielemento, React avrebbe evitato di ri-renderizzare ogni altro lielemento che non avesse la sua classNamemodifica, ma a quanto pare non è stato così.

Ho finito per definire una classe per gli Resultelementi, dove controlla esplicitamente se ogni Resultelemento deve essere rieseguito in base al fatto che sia stato precedentemente selezionato e in base all'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 è ora 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 migliorato leggermente le prestazioni , ma non è ancora abbastanza buono. Il fatto è che quando ho provato sulla versione di produzione di React le cose hanno funzionato senza intoppi, senza alcun ritardo.

LINEA DI FONDO

È normale una discrepanza così evidente tra le versioni di sviluppo e di produzione di React?

Sto capendo / facendo qualcosa di sbagliato quando penso a come React gestisce l'elenco?

AGGIORNAMENTO 14-11-2016

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

La soluzione è molto simile a quella proposta dalla risposta di AskarovBeknar , di seguito

AGGIORNAMENTO 14-4-2018

Poiché questa è apparentemente una domanda popolare e le cose sono progredite da quando è stata posta la domanda originale, mentre ti incoraggio a guardare il video collegato sopra, al fine di avere un'idea di un layout virtuale, ti incoraggio anche a usare React Virtualized biblioteca se non vuoi reinventare la ruota.


Cosa intendi per versione di sviluppo / produzione di React?
Dibesjr


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 negli elenchi di grandi dimensioni è la creazione di funzioni nel rendering. Avrà un calo delle prestazioni quando entri in elenchi giganti. Vorrei provare a vedere quanto tempo ci vuole per generare
quell'elenco

2
Penso che dovresti riconsiderare l'uso di Redux perché è esattamente ciò di cui hai bisogno qui (o qualsiasi tipo di implementazione del flusso). Dovresti
assolutamente

2
Dubito che un utente abbia alcun vantaggio nello scorrere 10000 risultati. Che cosa succede se si esegue il rendering solo dei primi 100 risultati o giù di lì e li si aggiorna in base alla query.
Koen.

Risposte:


18

Come per molte altre risposte a questa domanda, il problema principale risiede nel fatto che il rendering di così tanti elementi nel DOM mentre si filtra e si gestiscono gli eventi chiave sarà lento.

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

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

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

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

Questo non è affatto un production readycodice, 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 ...;)

reagire-grande-elenco-esempio

inserisci qui la descrizione dell'immagine


1
Mi dispiace davvero dover scegliere una sola risposta, sembrano tutti impegnati, ma al momento sono in vacanza senza PC e non posso davvero controllarli con l'attenzione che meritano. Ho scelto questo perché è abbastanza breve e al punto, da capire anche quando si legge da un telefono. Perchè debole lo so.
Dimitris Karagiannis

Cosa intendi per modificare il file host 127.0.0.1 * http://localhost:3001?
stackjlei

@stackjlei Penso che intendesse mappare 127.0.0.1 su localhost: 3001 in / etc / hosts
Maverick

16

La mia esperienza con un problema molto simile è che reagire soffre davvero se ci sono più di 100-200 o giù di lì componenti contemporaneamente nel DOM. Anche se stai molto attento (impostando tutte le tue chiavi e / o implementando un shouldComponentUpdatemetodo) a cambiare solo uno o due componenti in un re-rendering, sarai comunque in un mondo di dolore.

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

Quando scrivo le pagine ora provo a progettarle per ridurre al minimo il numero di componenti, un modo per farlo quando si rendono grandi elenchi di componenti è ... beh ... non eseguire il rendering di grandi elenchi di componenti.

Quello che voglio dire è: renderizza solo i componenti che puoi vedere attualmente, renderizza di più mentre scorri verso il basso, è improbabile che l'utente scorra verso il basso attraverso migliaia di componenti in alcun modo ... Spero.

Una grande libreria per fare questo è:

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

Con un fantastico how-to qui:

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

Temo che non rimuova i componenti che si trovano nella parte superiore della pagina, quindi se scorri abbastanza a 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 utilizzare questa libreria molto meglio di quanto posso fare qui. Spero di aver spiegato perché le liste grandi sono cattive, ma anche una soluzione.


2
Aggiornamento: il pacchetto che si trova in questa risposta non viene mantenuto. Un fork è installato su npmjs.com/package/react-infinite-scroller
Ali Al Amine

11

Prima di tutto, la differenza tra la versione di sviluppo e quella di produzione di React è enorme perché in produzione ci sono molti controlli di integrità ignorati (come la verifica dei tipi di oggetti di scena).

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

Ma prima di immergerti nel redux, devi apportare alcune modifiche al tuo codice React suddividendo i tuoi componenti in componenti più piccoli perché shouldComponentUpdatebypasserà totalmente il rendering dei bambini, quindi è un enorme guadagno .

Quando si hanno componenti più 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 mostra un elenco di concerti con potenziali duplicati concerti e devo 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, modificare il nome del concerto. Se non faccio nulla per un particolare elemento potenziale duplicato, verrà considerato duplicato e verrà eliminato.

Ecco come appare:

inserisci qui la descrizione dell'immagine

Ci sono fondamentalmente 4 componenti di rete (qui c'è solo una riga ma è per il bene dell'esempio):

inserisci qui la descrizione dell'immagine

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

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

Lezioni apprese facendo questa mini app quando si lavora con un enorme set di dati

  • I componenti React funzionano meglio quando sono mantenuti piccoli
  • La riselezione diventa molto utile per evitare il ricalcolo e mantenere lo stesso oggetto di riferimento (quando si usa immutable.js) con gli stessi argomenti.
  • Crea connectcomponente ED per 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 fanno uso
  • L'utilizzo della funzione fabric per creare mapDispatchToProps quando è necessario solo il supporto iniziale fornito ownPropsè necessario per evitare inutili ri-rendering
  • Reagisci e riduci definitivamente il rock insieme!

2
Non penso che l'aggiunta di una dipendenza al redux sia necessaria per risolvere il problema dell'OP, ulteriori azioni di invio per filtrare il suo set di risultati non farebbero che aggravare il problema, i dispacci non sono così economici come potresti pensare, gestendo questa particolare situazione con il componente locale stato è l'approccio più efficiente
deowk

4
  1. React nella versione di sviluppo controlla i tipi di ogni componente per facilitare il processo di sviluppo, mentre in produzione viene omesso.

  2. Il filtraggio dell'elenco di 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 quella di utilizzare il metodo antirimbalzo per ritardare l'esecuzione della funzione di filtro fino alla scadenza del ritardo.

  3. Un altro problema potrebbe essere l'enorme lista stessa. È possibile creare layout virtuale e riutilizzare gli elementi creati sostituendo semplicemente i dati. Fondamentalmente crei un componente contenitore scorrevole ad altezza fissa, all'interno del quale posizionerai il contenitore elenco. L'altezza del contenitore dell'elenco deve essere impostata manualmente (itemHeight * numberOfItems) a seconda della lunghezza dell'elenco visibile, per avere una barra di scorrimento funzionante. Quindi crea alcuni componenti di oggetti in modo che riempiano l'altezza dei contenitori scorrevoli e magari aggiungano uno o due effetti di elenco continuo imitazione in più. rendili in posizione assoluta e durante lo scorrimento sposta la loro posizione in modo che imiti l'elenco continuo (penso che scoprirai come implementarlo :)

  4. Un'altra cosa è che scrivere su DOM è anche un'operazione costosa, specialmente se lo fai male. Puoi utilizzare la tela per visualizzare gli elenchi e creare un'esperienza fluida durante lo scorrimento. Controlla i componenti della tela di reazione. Ho sentito che hanno già lavorato sulle liste.


Qualche info in merito React in development? e perché controlla i prototipi di ogni componente?
Liuuil

4

Dai un'occhiata a React Virtualized Select, è progettato per risolvere questo problema e si comporta in modo impressionante nella mia esperienza. Dalla descrizione:

HOC che utilizza React-Virtualized e React-Select per visualizzare grandi elenchi di opzioni in un menu a discesa

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


4

Come ho accennato nel mio commento , dubito che gli utenti abbiano bisogno di tutti quei 10000 risultati contemporaneamente nel browser.

Cosa succede se sfoglia i risultati e visualizzi sempre solo un elenco di 10 risultati.

Ho creato un esempio utilizzando questa tecnica, senza utilizzare altre librerie come Redux. Attualmente solo con la navigazione da tastiera, ma potrebbe essere facilmente esteso per lavorare anche sullo scorrimento.

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

Il succo sta nel tenere traccia di starte del selectedrisultato e spostare quelli sull'interazione con la 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})
  }
},

Passando semplicemente 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 tagliando i risultati in base starte limitnel rendermetodo:

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

Violino contenente un esempio funzionante completo: https://jsfiddle.net/koenpunt/hm1xnpqk/


3

Prova a filtrare prima di caricare nel componente React e mostra solo una quantità ragionevole di elementi nel componente e caricane di più su richiesta. Nessuno può visualizzare così tanti elementi contemporaneamente.

Non credo che lo siate, ma non usate gli indici come chiavi .

Per scoprire il vero motivo per cui le versioni di sviluppo e di produzione sono diverse, potresti provare il profilingtuo codice.

Carica la tua pagina, avvia la registrazione, esegui una modifica, interrompi la registrazione e quindi controlla i tempi. Consulta qui le istruzioni per la profilazione delle prestazioni in Chrome .


2

Per chiunque abbia problemi con questo problema ho scritto un componente react-big-list che gestisce elenchi fino a 1 milione di record.

Inoltre, include alcune fantasiose funzionalità extra come:

  • Ordinamento
  • Caching
  • Filtro personalizzato
  • ...

Lo stiamo usando in produzione in alcune app e funziona alla grande.


Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.