Desempenho de grande lista com React

86

Estou implementando uma lista filtrável com React. A estrutura da lista é mostrada na imagem abaixo.

insira a descrição da imagem aqui

PREMISSA

Aqui está uma descrição de como ele deve funcionar:

  • O estado reside no componente de nível mais alto, o Searchcomponente.
  • 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 para
  • currentlySelectedIndex que é o índice do elemento atualmente selecionado na lista filtrada.

  • O usuário digita mais de 2 letras no Inputcomponente, a matriz é filtrada e para cada entrada na matriz filtrada um Resultcomponente é renderizado

  • Cada Resultcomponente 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 Inputcomponente estiver em foco, as currentlySelectedIndexalterações serão feitas com base na filteredmatriz. Isso faz com que o Resultcomponente 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 filesarray 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 Resultelementos e estava apenas fazendo a lista em tempo real, a cada renderização do 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 você pode ver, toda vez que o for currentlySelectedIndexalterado, isso causará uma nova renderização e a lista será recriada a cada vez. Eu pensei que, como eu havia definido um keyvalor para cada lielemento, o React evitaria a renderização de todos os outros lielementos que não tivessem sua classNamealteração, mas aparentemente não foi assim.

Acabei definindo uma classe para os Resultelementos, onde verifica explicitamente se cada Resultelemento 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.

Dimitris Karagiannis
fonte
O que você quer dizer com versão de desenvolvimento / produção de react?
Dibesjr
@Dibesjr facebook.github.io/react/…
Dimitris Karagiannis
Ah entendo, obrigado. Então, para responder a uma de suas perguntas, ele diz que há uma discrepância na otimização entre as versões. Uma coisa a se observar em grandes listas é a criação de funções em seu render. Ele terá um impacto de desempenho quando você entrar em listas gigantes. Eu tentaria ver quanto tempo leva para gerar essa lista usando suas ferramentas de desempenho facebook.github.io/react/docs/perf.html
Dibesjr
2
Eu acho que você deveria reconsiderar o uso do Redux porque é exatamente o que você precisa aqui (ou qualquer tipo de implementação de fluxo). Você deve definitivamente dar uma olhada nesta apresentação: Big List High Performance React & Redux
Pierre Criulanscy
2
Duvido que um usuário tenha qualquer benefício de rolar pelos 10.000 resultados. E daí se você renderizar apenas os 100 resultados mais ou menos e atualizá-los com base na consulta.
Koen.

Respostas:

18

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 readycó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

insira a descrição da imagem aqui

Deowk
fonte
1
Eu realmente me sinto mal por ter que escolher apenas uma resposta, todas elas parecem ter um esforço despendido, mas no momento estou de férias sem PC e não posso realmente verificá-las com a atenção que merecem. Eu escolhi este porque é curto o suficiente e direto ao ponto, para entender mesmo quando estiver lendo um telefone. Razão esfarrapada, eu sei.
Dimitris Karagiannis
O que você quer dizer com editar arquivo host 127.0.0.1 * http://localhost:3001?
stackjlei
@stackjlei Acho que ele quis dizer mapear 127.0.0.1 para localhost: 3001 em / etc / hosts
Maverick
16

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 shouldComponentUpdatemé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.

Ressonância
fonte
2
Atualização: o pacote que está nesta resposta não é mantido. Um fork está configurado em npmjs.com/package/react-infinite-scroller
Ali Al Amine
11

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 shouldComponentUpdatevai 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:

insira a descrição da imagem aqui

Existem basicamente 4 componentes principais (há apenas uma linha aqui, mas é por causa do exemplo):

insira a descrição da imagem aqui

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

  • Os componentes do React funcionam melhor quando são mantidos pequenos
  • Selecionar novamente tornou-se muito útil para evitar a recomputação e manter o mesmo objeto de referência (ao usar immutable.js) com os mesmos argumentos.
  • Crie connectum 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 usam
  • O uso da função de malha para criar mapDispatchToProps quando você precisa apenas do prop inicial fornecido ownPropsé necessário para evitar uma nova renderização inútil
  • React & redux rock definitivamente juntos!
Pierre Criulanscy
fonte
2
Não acho que adicionar uma dependência ao redux seja necessário para resolver o problema do OP, mais ações de despacho para filtrar seu conjunto de resultados só agravariam o problema, despachos não são tão baratos quanto você pode pensar, lidando com esta situação particular com componente local estado é a abordagem mais eficiente
deowk
4
  1. O React na versão de desenvolvimento verifica os proptypes de cada componente para facilitar o processo de desenvolvimento, enquanto na produção é omitido.

  2. 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.

  3. 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 :)

  4. 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.

AskarovBeknar
fonte
Qualquer informação sobre React in development? e por que verifica os protótipos de cada componente?
Liuuil
4

Confira React Virtualized Select, ele foi projetado para resolver esse problema e tem um desempenho impressionante na minha experiência. Da descrição:

HOC que usa react-virtualized e react-select para exibir grandes listas de opções em uma lista suspensa

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

Madbreaks
fonte
4

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 starte do selectedresultado 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 starte limitno 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>
  )
}

Fiddle contendo um exemplo completo de trabalho: https://jsfiddle.net/koenpunt/hm1xnpqk/

Koen.
fonte
3

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 profilingseu 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 .

RationalDev gosta de GoFundMonica
fonte
2

Para qualquer pessoa que esteja lutando com esse problema, escrevi um componente react-big-listque lida com listas de até 1 milhão de registros.

Além disso, vem com alguns recursos extras sofisticados, como:

  • Ordenação
  • Cache
  • Filtragem personalizada
  • ...

Estamos usando em produção em alguns aplicativos e funciona muito bem.

Meemaw
fonte