Estou implementando uma lista filtrável com React. A estrutura da lista é mostrada na imagem abaixo.
PREMISSA
Aqui está uma descrição de como ele deve funcionar:
- O estado reside no componente de nível mais alto, o
Search
componente. - O estado é descrito da seguinte forma:
{ visível: booleano, arquivos: array, filtrado: matriz, consulta: string, currentSelectedIndex: integer }
files
é um array potencialmente muito grande contendo caminhos de arquivo (10.000 entradas é um número plausível).filtered
é a matriz filtrada após o usuário digitar pelo menos 2 caracteres. Eu sei que são dados derivados e, como tal, um argumento poderia ser feito sobre como armazená-los no estado, mas é necessário paracurrentlySelectedIndex
que é o índice do elemento atualmente selecionado na lista filtrada.O usuário digita mais de 2 letras no
Input
componente, a matriz é filtrada e para cada entrada na matriz filtrada umResult
componente é renderizadoCada
Result
componente está exibindo o caminho completo que correspondeu parcialmente à consulta e a parte da correspondência parcial do caminho é destacada. Por exemplo, o DOM de um componente Result, se o usuário tivesse digitado 'le' seria algo assim:<li>this/is/a/fi<strong>le</strong>/path</li>
- Se o usuário pressionar as teclas para cima ou para baixo enquanto o
Input
componente estiver em foco, ascurrentlySelectedIndex
alterações serão feitas com base nafiltered
matriz. Isso faz com que oResult
componente que corresponde ao índice seja marcado como selecionado, causando uma nova renderização
PROBLEMA
Inicialmente testei isso com um array pequeno o suficiente de files
, usando a versão de desenvolvimento do React, e tudo funcionou bem.
O problema apareceu quando tive que lidar com um files
array tão grande quanto 10.000 entradas. Digitar 2 letras na entrada geraria uma grande lista e quando eu pressionasse as teclas para cima e para baixo para navegar, ficaria muito lento.
No início eu não tinha um componente definido para os Result
elementos e estava apenas fazendo a lista em tempo real, a cada renderização do 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 você pode ver, toda vez que o for currentlySelectedIndex
alterado, isso causará uma nova renderização e a lista será recriada a cada vez. Eu pensei que, como eu havia definido um key
valor para cada li
elemento, o React evitaria a renderização de todos os outros li
elementos que não tivessem sua className
alteração, mas aparentemente não foi assim.
Acabei definindo uma classe para os Result
elementos, onde verifica explicitamente se cada Result
elemento deve ser renderizado novamente com base em se foi selecionado anteriormente e com base na entrada do usuário atual:
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 a lista agora é criada assim:
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));
}
Isso tornou o desempenho um pouco melhor, mas ainda não é bom o suficiente. A coisa foi quando eu testei na versão de produção do React as coisas funcionaram como manteiga, sem lag.
BOTTOMLINE
Essa discrepância perceptível entre as versões de desenvolvimento e produção do React é normal?
Estou entendendo / fazendo algo errado quando penso em como o React gerencia a lista?
ATUALIZAÇÃO 14-11-2016
Encontrei esta apresentação de Michael Jackson, onde ele aborda um assunto muito semelhante a este: https://youtu.be/7S8v8jfLb1Q?t=26m2s
A solução é muito semelhante à proposta pela resposta de AskarovBeknar , abaixo
ATUALIZAÇÃO 14-4-2018
Uma vez que esta é aparentemente uma pergunta popular e as coisas progrediram desde que a pergunta original foi feita, embora eu o encoraje a assistir ao vídeo no link acima, a fim de obter uma compreensão de um layout virtual, eu também encorajo você a usar o React Virtualized biblioteca se você não quiser reinventar a roda.
fonte
Respostas:
Tal como acontece com muitas das outras respostas a esta pergunta, o principal problema reside no fato de que renderizar tantos elementos no DOM enquanto filtra e manipula eventos-chave será lento.
Você não está fazendo nada inerentemente errado em relação ao React que está causando o problema, mas como muitos dos problemas relacionados ao desempenho, a IU também pode levar uma grande porcentagem da culpa.
Se sua IU não foi projetada com eficiência em mente, até ferramentas como o React, projetadas para ter desempenho, serão prejudicadas.
Filtrar o conjunto de resultados é um ótimo começo, conforme mencionado por @Koen
Eu brinquei um pouco com a ideia e criei um aplicativo de exemplo que ilustra como posso começar a resolver esse tipo de problema.
Isso não é de forma alguma um
production ready
código, mas ilustra o conceito de forma adequada e pode ser modificado para ser mais robusto, fique à vontade para dar uma olhada no código - espero, pelo menos, que ele lhe dê algumas ideias ...;)react-large-list-example
fonte
127.0.0.1 * http://localhost:3001
?Minha experiência com um problema muito semelhante é que o react realmente sofre se houver mais de 100-200 ou mais componentes no DOM ao mesmo tempo. Mesmo se você for muito cuidadoso (configurando todas as suas chaves e / ou implementando um
shouldComponentUpdate
método) para apenas alterar um ou dois componentes em uma re-renderização, você ainda estará em um mundo de ferimentos.A parte lenta da reação no momento é quando ele compara a diferença entre o DOM virtual e o DOM real. Se você tem milhares de componentes, mas atualiza apenas alguns, não importa, o react ainda tem uma grande diferença de operação para fazer entre os DOMs.
Quando escrevo páginas agora, tento projetá-las para minimizar o número de componentes; uma maneira de fazer isso ao renderizar grandes listas de componentes é ... bem ... não renderizar grandes listas de componentes.
O que quero dizer é: renderize apenas os componentes que você pode ver atualmente, renderize mais conforme você rola para baixo, seu usuário provavelmente não vai rolar para baixo em milhares de componentes de qualquer maneira ... Espero.
Uma ótima biblioteca para fazer isso é:
https://www.npmjs.com/package/react-infinite-scroll
Com um ótimo how-to aqui:
http://www.reactexamples.com/react-infinite-scroll/
Infelizmente, ele não remove os componentes que estão fora do topo da página, então, se você rolar por muito tempo, seus problemas de desempenho começarão a surgir novamente.
Sei que não é uma boa prática fornecer um link como resposta, mas os exemplos que eles fornecem vão explicar como usar essa biblioteca muito melhor do que posso fazer aqui. Espero ter explicado por que listas grandes são ruins, mas também uma solução alternativa.
fonte
Em primeiro lugar, a diferença entre a versão de desenvolvimento e produção do React é enorme porque na produção há muitas verificações de sanidade ignoradas (como verificação de tipos de suporte).
Então, eu acho que você deve reconsiderar o uso do Redux porque seria extremamente útil aqui para o que você precisa (ou qualquer tipo de implementação de fluxo). Você deve definitivamente dar uma olhada nesta apresentação: Big List High Performance React & Redux .
Mas antes de mergulhar no redux, você precisa fazer alguns ajustes em seu código React, dividindo seus componentes em componentes menores, porque
shouldComponentUpdate
vai contornar totalmente a renderização de filhos, então é um grande ganho .Quando você tem componentes mais granulares, pode controlar o estado com redux e react-redux para organizar melhor o fluxo de dados.
Recentemente, enfrentei um problema semelhante quando precisei renderizar mil linhas e ser capaz de modificar cada linha editando seu conteúdo. Este mini-aplicativo exibe uma lista de concertos com concertos em potencial duplicados e eu preciso escolher para cada duplicata em potencial se quiser marcar a duplicata em potencial como um show original (não uma duplicata) marcando a caixa de seleção e, se necessário, editar o nome do concerto. Se eu não fizer nada para um determinado item duplicado em potencial, ele será considerado duplicado e será excluído.
Aqui está o que parece:
Existem basicamente 4 componentes principais (há apenas uma linha aqui, mas é por causa do exemplo):
Aqui está o código completo (CodePen de trabalho: Huge List with React & Redux ) usando redux , react-redux , imutável , selecionar novamente e recompor :
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') );
Lições aprendidas ao fazer este miniaplicativo ao trabalhar com um grande conjunto de dados
connect
um componente ed para o componente que está mais próximo dos dados de que eles precisam para evitar que o componente passe apenas para baixo props que eles não usamownProps
é necessário para evitar uma nova renderização inútilfonte
O React na versão de desenvolvimento verifica os proptypes de cada componente para facilitar o processo de desenvolvimento, enquanto na produção é omitido.
Filtrar lista de strings é uma operação muito cara para cada keyup. isso pode causar problemas de desempenho devido à natureza de um único encadeamento do JavaScript. A solução pode ser usar o método debounce para atrasar a execução da função de filtro até que o atraso expire.
Outro problema pode ser a própria lista enorme. Você pode criar um layout virtual e reutilizar os itens criados apenas substituindo os dados. Basicamente, você cria um componente de contêiner rolável com altura fixa, dentro do qual você colocará um contêiner de lista. A altura do contêiner da lista deve ser definida manualmente (itemHeight * numberOfItems) dependendo do comprimento da lista visível, para ter uma barra de rolagem funcionando. Em seguida, crie alguns componentes de item para que eles preencham a altura dos contêineres roláveis e talvez adicionem um ou dois efeitos de lista contínua de simulação. coloque-os em posição absoluta e, ao rolar, mova sua posição para que ela imite uma lista contínua (acho que você descobrirá como implementá-la :)
Mais uma coisa é que gravar no DOM também é uma operação cara, especialmente se você fizer isso errado. Você pode usar a tela para exibir listas e criar uma experiência suave na rolagem. Verifique os componentes da tela de reação. Ouvi dizer que eles já fizeram algum trabalho nas Listas.
fonte
React in development
? e por que verifica os protótipos de cada componente?Confira React Virtualized Select, ele foi projetado para resolver esse problema e tem um desempenho impressionante na minha experiência. Da descrição:
https://github.com/bvaughn/react-virtualized-select
fonte
Como mencionei em meu comentário , duvido que os usuários precisem de todos esses 10.000 resultados no navegador de uma vez.
E se você percorrer os resultados e sempre mostrar apenas uma lista de 10 resultados.
Eu criado um exemplo usando esta técnica, sem usar qualquer outra biblioteca como Redux. Atualmente apenas com navegação por teclado, mas pode ser facilmente estendido para funcionar na rolagem também.
O exemplo existe de 3 componentes, o aplicativo de contêiner, um componente de pesquisa e um componente de lista. Quase toda a lógica foi movida para o componente do contêiner.
A essência está em manter o controle do
start
e doselected
resultado e alterá-los na interação do 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}) } },
Enquanto simplesmente passa todos os arquivos por um 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 cortando os resultados com base em
start
elimit
norender
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> ) }
Fiddle contendo um exemplo completo de trabalho: https://jsfiddle.net/koenpunt/hm1xnpqk/
fonte
Tente filtrar antes de carregar no componente React e mostrar apenas uma quantidade razoável de itens no componente e carregar mais sob demanda. Ninguém pode ver tantos itens ao mesmo tempo.
Acho que não, mas não use índices como chaves .
Para descobrir o verdadeiro motivo pelo qual as versões de desenvolvimento e produção são diferentes, você pode tentar
profiling
seu código.Carregue sua página, comece a gravar, faça uma alteração, pare de gravar e verifique os tempos. Veja aqui as instruções para criação de perfis de desempenho no Chrome .
fonte
Para qualquer pessoa que esteja lutando com esse problema, escrevi um componente
react-big-list
que lida com listas de até 1 milhão de registros.Além disso, vem com alguns recursos extras sofisticados, como:
Estamos usando em produção em alguns aplicativos e funciona muito bem.
fonte
React tem uma
react-window
biblioteca recomendada : https://www.npmjs.com/package/react-windowÉ melhor do que
react-vitualized
. Você pode experimentá-lofonte