Gran rendimiento de lista con React

86

Estoy en el proceso de implementar una lista filtrable con React. La estructura de la lista es como se muestra en la imagen a continuación.

ingrese la descripción de la imagen aquí

PREMISA

Aquí hay una descripción de cómo se supone que funciona:

  • El estado reside en el componente de más alto nivel, el Searchcomponente.
  • El estado se describe de la siguiente manera:
{
    visible: booleano,
    archivos: matriz,
    filtrado: matriz,
    consulta: cadena,
    currentSelectedIndex: integer
}
  • files es una matriz potencialmente muy grande que contiene rutas de archivo (10000 entradas es un número plausible).
  • filteredes la matriz filtrada después de que el usuario escribe al menos 2 caracteres. Sé que son datos derivados y, como tal, se podría argumentar sobre almacenarlos en el estado, pero es necesario para
  • currentlySelectedIndex que es el índice del elemento seleccionado actualmente de la lista filtrada.

  • El usuario escribe más de 2 letras en el Inputcomponente, la matriz se filtra y por cada entrada en la matriz filtrada Resultse representa un componente

  • Cada Resultcomponente muestra la ruta completa que coincidió parcialmente con la consulta, y la parte de coincidencia parcial de la ruta está resaltada. Por ejemplo, el DOM de un componente Result, si el usuario hubiera escrito 'le' sería algo como esto:

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

  • Si el usuario presiona las teclas arriba o abajo mientras el Inputcomponente está enfocado, los currentlySelectedIndexcambios se basan en la filteredmatriz. Esto hace que el Resultcomponente que coincide con el índice se marque como seleccionado provocando una re-renderización

PROBLEMA

Inicialmente probé esto con una matriz lo suficientemente pequeña files, usando la versión de desarrollo de React, y todo funcionó bien.

El problema apareció cuando tuve que lidiar con una filesmatriz de hasta 10000 entradas. Escribir 2 letras en la entrada generaría una gran lista y cuando presioné las teclas de arriba y abajo para navegar sería muy lento.

Al principio, no tenía un componente definido para los Resultelementos y simplemente estaba haciendo la lista sobre la marcha, en cada renderizado del Searchcomponente, como tal:

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

Como puede ver, cada vez que se currentlySelectedIndexcambia, se vuelve a generar y la lista se vuelve a crear cada vez. Pensé que dado que había establecido un keyvalor en cada lielemento, React evitaría volver a renderizar todos los demás lielementos que no tuvieran su classNamecambio, pero aparentemente no fue así.

Terminé definiendo una clase para los Resultelementos, donde verifica explícitamente si cada Resultelemento debe volver a renderizarse en función de si se seleccionó previamente y en función de la entrada actual del usuario:

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

Y la lista ahora se crea como tal:

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

Esto hizo que el rendimiento fuera un poco mejor, pero aún no es lo suficientemente bueno. La cosa es que cuando probé en la versión de producción de React, las cosas funcionaron perfectamente, sin ningún retraso.

LÍNEA DE FONDO

¿Es normal una discrepancia tan notable entre las versiones de desarrollo y producción de React?

¿Estoy entendiendo / haciendo algo mal cuando pienso en cómo React gestiona la lista?

ACTUALIZACIÓN 14-11-2016

He encontrado esta presentación de Michael Jackson, donde aborda un tema muy similar a este: https://youtu.be/7S8v8jfLb1Q?t=26m2s

La solución es muy similar a la propuesta por la respuesta de AskarovBeknar , a continuación

ACTUALIZACIÓN 14-4-2018

Dado que esta es aparentemente una pregunta popular y las cosas han progresado desde que se hizo la pregunta original, aunque te animo a que veas el video vinculado anteriormente, para que comprendas un diseño virtual, también te animo a usar React Virtualized biblioteca si no quieres reinventar la rueda.

Dimitris Karagiannis
fuente
¿A qué te refieres con versión de desarrollo / producción de react?
Dibesjr
@Dibesjr facebook.github.io/react/…
Dimitris Karagiannis
Ah, ya veo, gracias. Entonces, para responder a una de sus preguntas, dice que hay una discrepancia en la optimización entre las versiones. Una cosa a tener en cuenta en las listas grandes es la creación de funciones en su render. Tendrá un impacto en el rendimiento cuando entre en listas gigantes. Intentaría ver cuánto tiempo lleva generar esa lista usando sus herramientas de perf. Facebook.github.io/react/docs/perf.html
Dibesjr
2
Creo que debería reconsiderar el uso de Redux porque es exactamente lo que necesita aquí (o cualquier tipo de implementación de flujo). Definitivamente debería echar un vistazo a esta presentación: Big List High Performance React & Redux
Pierre Criulanscy
2
Dudo que un usuario tenga algún beneficio de desplazarse por 10000 resultados. Entonces, ¿qué pasa si solo procesa los 100 resultados principales más o menos y los actualiza según la consulta?
Koen.

Respuestas:

18

Al igual que con muchas de las otras respuestas a esta pregunta, el principal problema radica en el hecho de que renderizar tantos elementos en el DOM mientras se filtran y manejan eventos clave va a ser lento.

No está haciendo nada intrínsecamente incorrecto con respecto a React que está causando el problema, pero al igual que muchos de los problemas relacionados con el rendimiento, la interfaz de usuario también puede tener un gran porcentaje de culpa.

Si su interfaz de usuario no está diseñada teniendo en cuenta la eficiencia, incluso las herramientas como React que están diseñadas para ser eficaces sufrirán.

Filtrar el conjunto de resultados es un gran comienzo como lo menciona @Koen

Jugué un poco con la idea y creé una aplicación de ejemplo que ilustra cómo podría comenzar a abordar este tipo de problema.

Esto de ninguna manera es un production readycódigo, pero ilustra el concepto de manera adecuada y se puede modificar para que sea más robusto, siéntase libre de echarle un vistazo al código - espero que al menos le dé algunas ideas ...;)

reaccionar-lista-grande-ejemplo

ingrese la descripción de la imagen aquí

Deowk
fuente
1
Realmente me siento mal por tener que elegir solo una respuesta, parece que a todos les cuesta trabajo, pero actualmente estoy de vacaciones sin PC y realmente no puedo revisarlos con la atención que merecen. Elegí este porque es lo suficientemente corto y va al grano, para entenderlo incluso cuando se lee desde un teléfono. Poca razón, lo sé.
Dimitris Karagiannis
¿Qué quiere decir editar archivo de host 127.0.0.1 * http://localhost:3001?
stackjlei
@stackjlei Creo que se refería a mapear 127.0.0.1 a localhost: 3001 en / etc / hosts
Maverick
16

Mi experiencia con un problema muy similar es que la reacción realmente sufre si hay más de 100-200 o más componentes en el DOM a la vez. Incluso si tiene mucho cuidado (al configurar todas sus claves y / o implementar un shouldComponentUpdatemétodo) para cambiar solo uno o dos componentes en una repetición, todavía estará en un mundo de dolor.

La parte lenta de reaccionar en este momento es cuando compara la diferencia entre el DOM virtual y el DOM real. Si tiene miles de componentes pero solo actualiza un par, no importa, reaccionar aún tiene una operación de diferencia masiva que hacer entre los DOM.

Cuando escribo páginas ahora trato de diseñarlas para minimizar el número de componentes, una forma de hacer esto cuando renderizo grandes listas de componentes es ... bueno ... no renderizar grandes listas de componentes.

Lo que quiero decir es: solo renderice los componentes que puede ver actualmente, renderice más a medida que se desplaza hacia abajo, es probable que su usuario no se desplace hacia abajo a través de miles de componentes de ninguna manera ... espero.

Una gran biblioteca para hacer esto es:

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

Con un gran tutorial aquí:

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

Sin embargo, me temo que no elimina los componentes que están fuera de la parte superior de la página, por lo que si se desplaza el tiempo suficiente, comenzarán a surgir problemas de rendimiento.

Sé que no es una buena práctica proporcionar un enlace como respuesta, pero los ejemplos que brindan explicarán cómo usar esta biblioteca mucho mejor de lo que puedo aquí. Ojalá haya explicado por qué las listas grandes son malas, pero también una solución.

Resonancia
fuente
2
Actualización: el paquete que está en esta respuesta no se mantiene. Se configura una bifurcación en npmjs.com/package/react-infinite-scroller
Ali Al Amine
11

En primer lugar, la diferencia entre la versión de desarrollo y la de producción de React es enorme porque en la producción hay muchos controles de cordura omitidos (como la verificación de tipos de accesorios).

Entonces, creo que debería reconsiderar el uso de Redux porque sería extremadamente útil aquí para lo que necesita (o cualquier tipo de implementación de flujo). Definitivamente debería echar un vistazo a esta presentación: Big List High Performance React & Redux .

Pero antes de sumergirse en redux, debe realizar algunos ajustes en su código de React dividiendo sus componentes en componentes más pequeños porque shouldComponentUpdateevitará totalmente la representación de los niños, por lo que es una gran ganancia .

Cuando tiene componentes más granulares, puede manejar el estado con redux y react-redux para organizar mejor el flujo de datos.

Recientemente me enfrenté a un problema similar cuando necesitaba renderizar mil filas y poder modificar cada fila editando su contenido. Esta mini aplicación muestra una lista de conciertos con posibles conciertos duplicados y necesito elegir para cada posible duplicado si quiero marcar el posible duplicado como un concierto original (no un duplicado) marcando la casilla de verificación y, si es necesario, editar el nombre del concierto. Si no hago nada por un elemento potencial duplicado en particular, se considerará duplicado y se eliminará.

Así es como se ve:

ingrese la descripción de la imagen aquí

Básicamente, hay 4 componentes de red (solo hay una fila aquí, pero es por el bien del ejemplo):

ingrese la descripción de la imagen aquí

Aquí está el código completo (CodePen en funcionamiento: Lista enorme con React & Redux ) usando redux , react-redux , inmutable , reselect y 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')
);

Lecciones aprendidas con esta mini aplicación cuando se trabaja con un gran conjunto de datos

  • Los componentes de React funcionan mejor cuando se mantienen pequeños
  • Volver a seleccionar se vuelve muy útil para evitar el recálculo y mantener el mismo objeto de referencia (cuando se usa inmutable.js) dados los mismos argumentos.
  • Crear un connectcomponente ed para el componente que es el más cercano a los datos que necesitan para evitar que el componente solo transmita accesorios que no usan
  • El uso de la función de tejido para crear mapDispatchToProps cuando solo necesita el accesorio inicial proporcionado ownPropses necesario para evitar una re-renderización inútil
  • ¡React & redux definitivamente rockean juntos!
Pierre Criulanscy
fuente
2
No creo que sea necesario agregar una dependencia a redux para resolver el problema del OP, más acciones de envío para filtrar su conjunto de resultados solo agravarían el problema, los envíos no son tan económicos como podría pensar, manejando esta situación particular con el componente local estado es el enfoque más eficiente
deowk
4
  1. React en la versión de desarrollo comprueba los tipos de propiedad de cada componente para facilitar el proceso de desarrollo, mientras que en producción se omite.

  2. Filtrar la lista de cadenas es una operación muy costosa para cada tecla. podría causar problemas de rendimiento debido a la naturaleza de un solo subproceso de JavaScript. La solución podría ser utilizar el método antirrebote para retrasar la ejecución de su función de filtro hasta que expire el retraso.

  3. Otro problema podría ser la enorme lista en sí. Puede crear un diseño virtual y reutilizar elementos creados simplemente reemplazando datos. Básicamente, crea un componente de contenedor desplazable con altura fija, dentro del cual colocará el contenedor de lista. La altura del contenedor de la lista debe establecerse manualmente (itemHeight * numberOfItems) dependiendo de la longitud de la lista visible, para que funcione una barra de desplazamiento. Luego, cree algunos componentes de elementos para que llenen la altura de los contenedores desplazables y tal vez agreguen uno o dos efectos de lista continua de imitación adicionales. hazlos en posición absoluta y en el desplazamiento simplemente mueve su posición para que imite la lista continua (creo que descubrirás cómo implementarlo :)

  4. Una cosa más es que escribir en DOM también es una operación costosa, especialmente si lo hace mal. Puede usar el lienzo para mostrar listas y crear una experiencia fluida en el desplazamiento. Verifique los componentes de react-canvas. Escuché que ya han trabajado en Listas.

AskarovBeknar
fuente
¿Alguna información sobre React in development? y ¿por qué comprueba los protoytpes de cada componente?
Liuuil
4

Eche un vistazo a React Virtualized Select, está diseñado para abordar este problema y funciona de manera impresionante en mi experiencia. De la descripción:

HOC que usa react-virtualized y react-select para mostrar grandes listas de opciones en un menú desplegable

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

Madbreaks
fuente
4

Como mencioné en mi comentario , dudo que los usuarios necesiten todos esos 10000 resultados en el navegador a la vez.

¿Qué sucede si hojea los resultados y siempre muestra una lista de 10 resultados?

He creado un ejemplo usando esta técnica, sin usar ninguna otra biblioteca como Redux. Actualmente solo con navegación por teclado, pero podría extenderse fácilmente para trabajar también en el desplazamiento.

El ejemplo consta de 3 componentes, la aplicación contenedora, un componente de búsqueda y un componente de lista. Casi toda la lógica se ha trasladado al componente contenedor.

La esencia radica en realizar un seguimiento del starty el selectedresultado, y cambiarlos en la interacción del teclado.

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

Mientras simplemente pasa todos los archivos a través de 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
  });
},

Y dividiendo los resultados basados ​​en starty limiten el rendermétodo:

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

Violín que contiene un ejemplo de trabajo completo: https://jsfiddle.net/koenpunt/hm1xnpqk/

Koen.
fuente
3

Intente filtrar antes de cargar en el componente React y solo muestre una cantidad razonable de elementos en el componente y cargue más a pedido. Nadie puede ver tantos elementos a la vez.

No creo que lo seas, pero no uses índices como claves .

Para descubrir la verdadera razón por la que las versiones de desarrollo y producción son diferentes, puede probar profilingsu código.

Cargue su página, comience a grabar, realice un cambio, detenga la grabación y luego verifique los tiempos. Consulte aquí las instrucciones para la creación de perfiles de rendimiento en Chrome .

A RationalDev le gusta GoFundMonica
fuente
2

Para cualquiera que esté luchando con este problema, he escrito un componente react-big-listque maneja listas de hasta 1 millón de registros.

Además de eso, viene con algunas características adicionales elegantes como:

  • Clasificación
  • Almacenamiento en caché
  • Filtrado personalizado
  • ...

Lo estamos usando en producción en bastantes aplicaciones y funciona muy bien.

Meemaw
fuente