Reactによるビッグリストのパフォーマンス


86

私はReactでフィルター可能なリストを実装しているところです。リストの構造は下の画像のようになっています。

ここに画像の説明を入力してください

前提

これがどのように機能するかについての説明です:

  • 状態は、最上位のコンポーネントであるSearchコンポーネントに存在します。
  • 状態は次のように記述されます。
{{
    表示:ブール値、
    ファイル:配列、
    フィルタリング:配列、
    クエリ文字列、
    currentSelectedIndex:整数
}
  • files は、ファイルパスを含む潜在的に非常に大きな配列です(10000エントリが妥当な数です)。
  • filteredユーザーが少なくとも2文字を入力した後、フィルター処理された配列です。私はそれが派生データであることを知っているので、それを状態に保存することについて議論することができますが、それは
  • currentlySelectedIndex これは、フィルタリングされたリストから現在選択されている要素のインデックスです。

  • ユーザーがInputコンポーネントに2文字以上を入力すると、配列がフィルタリングされ、フィルタリングされた配列のエントリごとにResultコンポーネントがレンダリングされます。

  • Resultコンポーネントは、クエリに部分的に一致した完全なパスを表示しており、パスの部分的に一致した部分が強調表示されています。たとえば、結果コンポーネントのDOMは、ユーザーが「le」と入力した場合、次のようになります。

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

  • Inputコンポーネントがフォーカスされているときにユーザーが上キーまたは下キーを押すcurrentlySelectedIndexと、filtered配列に基づいて変更が行われます。これにより、Resultインデックスに一致するコンポーネントが選択済みとしてマークされ、再レンダリングが発生します。

問題

最初はfiles、Reactの開発バージョンを使用して、十分に小さい配列でこれをテストしましたが、すべて正常に機能しました。

この問題は、files10000エントリもの大きな配列を処理する必要があるときに発生しました。入力に2文字を入力すると大きなリストが生成され、上下のキーを押してナビゲートすると非常に遅くなります。

最初は、要素に対して定義されたコンポーネントResultがなく、Searchコンポーネントの各レンダリングで、その場でリストを作成するだけでした。

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

お分かりのように、currentlySelectedIndex変更するたびに再レンダリングが発生し、リストは毎回再作成されます。keyli要素に値を設定したので、ReactはliclassName変更されていない他のすべての要素の再レンダリングを回避できると思いましたが、明らかにそうではありませんでした。

最終Result的に要素のクラスを定義しました。このクラスでは、Result以前に選択されたかどうかと現在のユーザー入力に基づいて、各要素を再レンダリングする必要があるかどうかを明示的にチェックします。

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

そして、リストは次のように作成されます。

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

これによりパフォーマンスはわずかに向上しましたが、それでも十分ではありません。私がReactの製品版でテストしたとき、物事はバターのようにスムーズに機能し、ラグはまったくありませんでした。

ボトムライン

Reactの開発バージョンと製品バージョンの間のそのような顕著な不一致は正常ですか?

Reactがリストをどのように管理するかを考えるとき、私は何か間違ったことを理解/実行していますか?

2016年11月14日更新

マイケルジャクソンのこのプレゼンテーションを見つけました。彼はこれと非常によく似た問題に取り組んでいますhttps//youtu.be/7S8v8jfLb1Q?t = 26m2s

解決策は、以下のAskarovBeknarの回答によって提案されたものと非常に似ています。

更新14-4-2018

これは明らかに人気のある質問であり、元の質問から状況が進んでいるため、上記のリンク先のビデオをご覧になることをお勧めしますが、仮想レイアウトを把握するために、ReactVirtualizedを使用することもお勧めします。車輪の再発明をしたくない場合はライブラリ。


reactの開発/本番バージョンとはどういう意味ですか?
Dibesjr 2016年


なるほど、ありがとう。したがって、あなたの質問の1つに答えるために、バージョン間で最適化に不一致があると言われています。大きなリストで注意すべきことの1つは、レンダリングで関数を作成することです。巨大なリストに入ると、パフォーマンスが低下します。私が試してみて、それが彼らのPERFのツールを使用してそのリストを生成するのにかかる時間の長さ参照してくださいでしょうfacebook.github.io/react/docs/perf.html
Dibesjr

2
Reduxの使用は、まさにここで必要なもの(またはあらゆる種類のフラックス実装)であるため、再検討する必要があると思います。このプレゼンテーションを
ぜひご覧ください

2
ユーザーが10000件の結果をスクロールするメリットがあるとは思えません。したがって、上位100件程度の結果のみをレンダリングし、クエリに基づいてこれらを更新するとどうなりますか。
公園。

回答:


18

この質問に対する他の多くの回答と同様に、主な問題は、主要なイベントのフィルタリングと処理を行っている間、DOMで非常に多くの要素をレンダリングするのが遅くなるという事実にあります。

問題の原因となっているReactに関して本質的に悪いことは何もしていませんが、パフォーマンスに関連する多くの問題と同様に、UIも大きな割合を占める可能性があります。

UIが効率を考慮して設計されていない場合、パフォーマンスを向上させるように設計されたReactのようなツールでさえ問題が発生します。

@Koenが述べたように、結果セットのフィルタリングは素晴らしいスタートです。

私はこのアイデアを少し試してみて、この種の問題にどのように取り組み始めるかを示すサンプルアプリを作成しました。

これは決してproduction readyコードではありませんが、概念を適切に示しており、より堅牢になるように変更できます。コードを自由に見てください。少なくとも、いくつかのアイデアが得られることを願っています...;)

react-large-list-example

ここに画像の説明を入力してください


1
答えを1つだけ選ぶのは本当に気分が悪く、みんな努力しているようですが、今はパソコンがなくて休暇中なので、気をつけてチェックすることはできません。電話で読んでも理解できるくらい短いので、これを選びました。私が知っているラメの理由。
Dimitris Karagiannis 2016

ホストファイルを編集するとはどういう意味127.0.0.1 * http://localhost:3001ですか?
stackjlei 2017

@stackjlei私は彼がするマッピング127.0.0.1を意味だと思うのlocalhost:3001 / etc / hostsに
マーベリック

16

非常によく似た問題に関する私の経験では、一度に100〜200を超えるコンポーネントがDOMにあると、反応が実際に悪化します。shouldComponentUpdate再レンダリングで1つまたは2つのコンポーネントのみを変更するように(すべてのキーを設定したり、メソッドを実装したりすることによって)細心の注意を払っていても、依然として傷ついた世界になります。

現時点での反応の遅い部分は、仮想DOMと実際のDOMの違いを比較するときです。何千ものコンポーネントがあり、更新するのが2つしかない場合でも、問題はありません。reactには、DOM間で実行する操作が大きく異なります。

今私がページを書くとき、私はコンポーネントの数を最小にするようにそれらを設計しようとします、コンポーネントの大きなリストをレンダリングするときにこれを行う1つの方法は...まあ...コンポーネントの大きなリストをレンダリングしないことです。

つまり、現在表示されているコンポーネントのみをレンダリングし、下にスクロールするとさらにレンダリングされます。ユーザーが何千ものコンポーネントを下にスクロールする可能性はほとんどありません。

これを行うための優れたライブラリは次のとおりです。

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

ここに素晴らしいハウツーがあります:

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

ただし、ページの上部にないコンポーネントは削除されないので、十分に長くスクロールすると、パフォーマンスの問題が再び発生し始めます。

答えとしてリンクを提供することは良い習慣ではないことを私は知っていますが、彼らが提供する例は、私がここでできるよりもはるかにうまくこのライブラリを使用する方法を説明するでしょう。うまくいけば、大きなリストが悪い理由を説明しただけでなく、回避策も説明しました。


2
更新:この回答にあるパッケージは維持されません。フォークはnpmjs.com/package/react-infinite-scrollerに
Ali Al Amine

11

まず第一に、Reactの開発バージョンと本番バージョンの違いは非常に大きいです。本番環境では、バイパスされたサニティチェック(小道具の種類の検証など)が多数あるためです。

次に、Reduxの使用を再検討する必要があると思います。これは、ここで必要なもの(またはあらゆる種類のフラックスの実装)に非常に役立つためです。あなたは間違いなくこのプレゼンテーションを見る必要があります:Big List High Performance React&Redux

ただし、reduxに飛び込む前に、コンポーネントを小さなコンポーネントに分割してReactコードを調整する必要があります。shouldComponentUpdateこれは、子のレンダリングを完全にバイパスするため、大きなメリットがあります。

より詳細なコンポーネントがある場合は、reduxとreact-reduxを使用して状態を処理し、データフローをより適切に整理できます。

最近、1,000行をレンダリングし、その内容を編集して各行を変更できるようにする必要があるときに、同様の問題に直面していました。このミニアプリは、重複する可能性のあるコンサートのリストを表示します。チェックボックスをオンにして、重複する可能性のあるコンサートを元のコンサート(重複ではない)としてマークする場合は、重複する可能性のあるコンサートごとに選択する必要があります。必要に応じて、コンサートの名前。特定の潜在的な重複アイテムに対して何もしなかった場合、それは重複と見なされ、削除されます。

これがどのように見えるかです:

ここに画像の説明を入力してください

基本的に4つのメインコンポーネントがあります(ここには1つの行しかありませんが、例のためです):

ここに画像の説明を入力してください

これは、reduxreact-reduximmutablereselectrecomposeを使用した完全なコード(CodePen:ReactとReduxを使用した巨大なリスト)です:

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

巨大なデータセットを操作するときにこのミニアプリを実行することで学んだ教訓

  • Reactコンポーネントは、小さく保つと最も効果的に機能します
  • 再選択は、再計算を回避し、同じ引数を指定して同じ参照オブジェクト(immutable.jsを使用する場合)を維持するために非常に役立ちます。
  • connectコンポーネントが使用しない小道具のみを渡すことを避けるために必要なデータに最も近いコンポーネントのedコンポーネントを作成します
  • 与えられた最初の小道具だけが必要なときにmapDispatchToPropsを作成するためのファブリック関数の使用は、ownProps無駄な再レンダリングを避けるために必要です。
  • React&reduxは間違いなく一緒に揺れ動きます!

2
OPの問題を解決するためにreduxに依存関係を追加する必要はないと思います。結果セットをフィルタリングするためのディスパッチアクションをさらに増やすと、問題が悪化するだけです。ディスパッチは思ったほど安価ではなく、ローカルコンポーネントでこの特定の状況を処理します。状態は最も効率的なアプローチです
deowk 2016

4
  1. React in developmentバージョンは、各コンポーネントのproptypeをチェックして開発プロセスを容易にしますが、本番環境では省略されます。

  2. 文字列のリストのフィルタリングは、すべてのキーアップで非常にコストのかかる操作です。JavaScriptのシングルスレッドの性質により、パフォーマンスの問題が発生する可能性があります。解決策は、debounceメソッドを使用して、遅延が期限切れになるまでフィルター関数の実行を遅らせることです。

  3. もう1つの問題は、膨大なリスト自体である可能性があります。あなたは、作成することができ、仮想レイアウトをし、データだけを置き換える作成されたアイテムを再利用します。基本的に、固定の高さでスクロール可能なコンテナコンポーネントを作成し、その中にリストコンテナを配置します。スクロールバーを機能させるには、表示されるリストの長さに応じて、リストコンテナの高さを手動で設定する必要があります(itemHeight * numberOfItems)。次に、いくつかのアイテムコンポーネントを作成して、スクロール可能なコンテナの高さを埋め、1つまたは2つの連続リスト効果を模倣するようにします。それらを絶対位置にし、スクロール時にそれらの位置を移動して、連続リストを模倣します(実装方法がわかると思います:)

  4. もう1つ、DOMへの書き込みは、特に間違った操作を行うと、コストのかかる操作になります。キャンバスを使用してリストを表示し、スクロールでスムーズなエクスペリエンスを作成できます。react-canvasコンポーネントをチェックアウトします。彼らはすでにリストでいくつかの作業を行っていると聞きました。


についての情報はありReact in developmentますか?そして、なぜ各コンポーネントのプロトタイプをチェックするのですか?
Liuuil 2018年

4

React Virtualized Selectをチェックしてください。この問題に対処するように設計されており、私の経験では印象的なパフォーマンスを発揮します。説明から:

react-virtualizedとreact-selectを使用してドロップダウンにオプションの大きなリストを表示するHOC

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


4

コメントで述べたようにユーザーがブラウザでこれらの10000件すべての結果を一度に必要とするのではないかと思います。

結果をページングし、常に10件の結果のリストを表示するとどうなりますか。

私がきた例を作成したReduxのような他のライブラリを使用せずに、この技術を用いて。現在はキーボードナビゲーションのみですが、スクロールでも機能するように簡単に拡張できます。

この例には、コンテナアプリケーション、検索コンポーネント、リストコンポーネントの3つのコンポーネントがあります。ほとんどすべてのロジックがコンテナコンポーネントに移動されました。

要点は、startselected結果を追跡し、キーボード操作でそれらをシフトすることにあります。

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

すべてのファイルをフィルターに通すだけです。

updateResults: function() {
  var results = this.props.files.filter(function(file){
    return file.file.indexOf(this.state.query) > -1
  }, this)

  this.setState({
    results: results
  });
},

とに基づいて結果をスライスstartし、limitrenderの方法:

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

完全に機能する例を含むフィドル:https//jsfiddle.net/koenpunt/hm1xnpqk/


3

Reactコンポーネントにロードする前にフィルターを試して、コンポーネント内の適切な量のアイテムのみを表示し、オンデマンドでさらにロードしてください。一度にたくさんのアイテムを見ることができる人は誰もいません。

私はあなたがそうだとは思いませんが、キーとしてインデックスを使用しないでください

開発バージョンと製品バージョンが異なる本当の理由を見つけるにはprofiling、コードを試すことができます。

ページを読み込み、記録を開始し、変更を実行し、記録を停止してから、タイミングを確認します。Chromeでのパフォーマンスプロファイリングの手順については、こちらをご覧ください。


2

この問題に苦しんでいる人のために、私はreact-big-list最大100万レコードのリストを処理するコンポーネントを作成しました。

その上、次のようないくつかの凝った追加機能が付属しています。

  • 並べ替え
  • キャッシング
  • カスタムフィルタリング
  • ..。

かなりの数のアプリで本番環境で使用しており、うまく機能します。


弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.