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.
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
Search
componente. - 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).filtered
es 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 paracurrentlySelectedIndex
que es el índice del elemento seleccionado actualmente de la lista filtrada.El usuario escribe más de 2 letras en el
Input
componente, la matriz se filtra y por cada entrada en la matriz filtradaResult
se representa un componenteCada
Result
componente 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
Input
componente está enfocado, loscurrentlySelectedIndex
cambios se basan en lafiltered
matriz. Esto hace que elResult
componente 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 files
matriz 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 Result
elementos y simplemente estaba haciendo la lista sobre la marcha, en cada renderizado del Search
componente, 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 currentlySelectedIndex
cambia, se vuelve a generar y la lista se vuelve a crear cada vez. Pensé que dado que había establecido un key
valor en cada li
elemento, React evitaría volver a renderizar todos los demás li
elementos que no tuvieran su className
cambio, pero aparentemente no fue así.
Terminé definiendo una clase para los Result
elementos, donde verifica explícitamente si cada Result
elemento 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.
fuente
Respuestas:
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 ready
có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
fuente
127.0.0.1 * http://localhost:3001
?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
shouldComponentUpdate
mé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.
fuente
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
shouldComponentUpdate
evitará 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:
Básicamente, hay 4 componentes de red (solo hay una fila aquí, pero es por el bien del ejemplo):
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
connect
componente 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 usanownProps
es necesario para evitar una re-renderización inútilfuente
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.
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.
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 :)
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.
fuente
React in development
? y ¿por qué comprueba los protoytpes de cada componente?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:
https://github.com/bvaughn/react-virtualized-select
fuente
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
start
y elselected
resultado, 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
start
ylimit
en elrender
mé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/
fuente
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
profiling
su 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 .
fuente
Para cualquiera que esté luchando con este problema, he escrito un componente
react-big-list
que maneja listas de hasta 1 millón de registros.Además de eso, viene con algunas características adicionales elegantes como:
Lo estamos usando en producción en bastantes aplicaciones y funciona muy bien.
fuente
React tiene una
react-window
biblioteca recomendada : https://www.npmjs.com/package/react-windowEs mejor que
react-vitualized
. Puedes probarlofuente