Reactでフィルター可能なリストを実装しています。リストの構造は、次の図に示すとおりです。
前提
これがどのように機能するかについて説明します。
Search
コンポーネントにあります。{ 表示:ブール、 ファイル:配列、 フィルター:配列、 クエリ:文字列、 現在選択されたインデックス:整数 }
files
は、ファイルパスを含む潜在的に非常に大きな配列です(10000エントリはもっともらしい数です)。filtered
は、ユーザーが少なくとも2文字を入力した後のフィルターされた配列です。私はそれが派生データであることを知っており、そのような状態でそれを保存することについて議論することができますが、currentlySelectedIndex
これは、フィルタリングされたリストから現在選択されている要素のインデックスです。
ユーザーがInput
コンポーネントに3文字以上を入力すると、配列がフィルター処理され、フィルター処理された配列の各エントリに対してResult
コンポーネントがレンダリングされます
各Result
コンポーネントには、クエリに部分的に一致した完全なパスが表示され、パスの部分一致部分が強調表示されます。たとえば、ユーザーが「le」と入力した場合、ResultコンポーネントのDOMは次のようになります。
<li>this/is/a/fi<strong>le</strong>/path</li>
Input
コンポーネントがフォーカスされているときにユーザーが上下キーを押すと、currentlySelectedIndex
配列に基づいてfiltered
が変更されます。これにより、インデックスに一致するResult
コンポーネントが選択済みとしてマークされ、再レンダリングが行われます。問題
最初は、Reactの開発バージョンを使用して、files
の十分に小さい配列でこれをテストし、すべて正常に機能しました。
この問題は、10000エントリものfiles
配列を処理しなければならなかったときに現れました。入力で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
が変更されるたびに、再レンダリングが発生し、そのたびにリストが再作成されます。各key
要素にli
値を設定していたので、Reactはli
を持たない他のすべてのclassName
要素を再レンダリングすることを避けると思いました変更しますが、明らかにそうではありませんでした。
最終的に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の製品版でテストしたとき、スムーズにバターのように動作し、遅延はまったくありませんでした。
BOTTOMLINE
Reactの開発バージョンと本番バージョンの間にこのような顕著な相違はありますか?
Reactがリストをどのように管理するかを考えると、何かを理解している/間違っているのでしょうか?
2016年11月14日更新
私はマイケル・ジャクソンのこのプレゼンテーションを見つけました。彼はこの問題に非常に似た問題に取り組んでいます: https://youtu.be/7S8v8jfLb1Q?t=26m2s
解決策は、AskarovBeknarの answer で提案されているものと非常に似ています。
PDATE 14-4-2018
これは明らかに人気のある質問であり、元の質問が尋ねられてから物事が進行しているので、仮想レイアウトを把握するために、上記のリンクされたビデオを視聴することをお勧めしますが、 React Virtualized 車輪を再発明したくない場合はライブラリ。
この質問に対する他の多くの回答と同様に、主要な問題は、DOMで非常に多くの要素をレンダリングし、キーイベントを処理して処理するのが遅いという事実にあります。
問題を引き起こしているReactに関して本質的に悪いことをしているわけではありませんが、パフォーマンスに関連する多くの問題と同様に、UIが大きな割合を占める可能性があります。
UIが効率を念頭に置いて設計されていない場合、Reactのようなパフォーマンスを発揮するように設計されたツールでも問題が発生します。
@Koenが述べたように、結果セットのフィルタリングは素晴らしいスタートです
このアイデアを少し試してみて、この種の問題にどのように取り組むかを説明するサンプルアプリを作成しました。
これは決してproduction ready
コードではありませんが、概念を適切に示しており、より堅牢になるように変更することができます。コードを自由にご覧ください。 。;)
非常によく似た問題に関する私の経験では、DOMに一度に100〜200個以上のコンポーネントがあると、実際に反応が悪くなります。再レンダーで1つまたは2つのコンポーネントのみを変更するように(すべてのキーを設定する、および/またはshouldComponentUpdate
メソッドを実装することにより)非常に慎重であっても、まだ傷つける。
現時点での反応の遅い部分は、仮想DOMと実際のDOMの違いを比較するときです。数千のコンポーネントがあり、カップルを更新するだけであれば、それは問題ではなく、reactにはDOM間で行うべき大きな違いの操作があります。
現在、ページを作成するとき、コンポーネントの数を最小限に抑えるようにデザインしようとしています。コンポーネントの大きなリストをレンダリングするときにこれを行う方法の1つは、...コンポーネントの大きなリストをレンダリングしないことです。
つまり、現在表示されているコンポーネントのみをレンダリングし、下にスクロールするほどレンダリングします。ユーザーは何千ものコンポーネントを下にスクロールすることはないでしょう。
これを行うための優れたライブラリは次のとおりです。
https://www.npmjs.com/package/react-infinite-scroll
素晴らしいハウツーをここに:
http://www.reactexamples.com/react-infinite-scroll/
ただし、ページの上部にあるコンポーネントは削除されないので、十分に長くスクロールすると、パフォーマンスの問題が再発し始めます。
答えとしてリンクを提供するのは良い習慣ではないことを知っていますが、彼らが提供する例は、私がここでできるよりもはるかにこのライブラリを使用する方法を説明しようとしています。大きなリストが悪い理由を説明しただけでなく、回避策もあります。
第一に、Reactの開発版と本番版の違いは非常に大きくなります。本番環境では、サニティチェック(プロップタイプの検証など)がバイパスされるためです。
次に、必要なもの(またはあらゆる種類のフラックス実装)にとってReduxが非常に役立つため、Reduxの使用を再検討する必要があると思います。このプレゼンテーションを明確に見てください: Big List High Performance React&Redux 。
ただし、reduxに飛び込む前に、shouldComponentUpdate
はレンダリングを完全にバイパスするため、コンポーネントをより小さなコンポーネントに分割することにより、Reactコードを調整する必要があります。子なので、それは大きな利益です。
より詳細なコンポーネントがある場合は、reduxおよびreact-reduxを使用して状態を処理し、データフローをより適切に整理できます。
私は最近、1000行をレンダリングし、コンテンツを編集して各行を変更できるようにする必要があるときに、同様の問題に直面していました。このミニアプリには、潜在的な重複コンサートがあるコンサートのリストが表示されます。チェックボックスをオンにして、潜在的な重複を元のコンサート(重複ではない)としてマークし、必要に応じて、コンサートの名前。特定の重複する可能性のあるアイテムに対して何もしない場合、そのアイテムは重複していると見なされ、削除されます。
これは次のようなものです。
基本的に4つのメインコンポーネントがあります(ここには1行しかありませんが、例のためです)。
redux 、 react-redux を使用した完全なコード(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')
);
巨大なデータセットを操作するときにこのミニアプリを実行することで学んだ教訓
connect
edコンポーネントを作成しますownProps
で指定された初期プロップのみが必要な場合にmapDispatchToPropsを作成するためのファブリック関数の使用は、無駄な再レンダリングを回避するために必要です開発バージョンではReactを使用して各コンポーネントのproptypeをチェックし、開発プロセスを容易にしますが、本番環境では省略します。
文字列のリストのフィルタリングは、キーアップごとに非常に高価な操作です。 JavaScriptのシングルスレッドの性質により、パフォーマンスの問題が発生する可能性があります。解決策は、デバウンスメソッドを使用して、遅延が期限切れになるまでフィルター関数の実行を遅らせることです。
別の問題は、巨大なリスト自体かもしれません。 仮想レイアウトを作成し、作成したアイテムをデータを置き換えるだけで再利用できます。基本的に、高さを固定したスクロール可能なコンテナコンポーネントを作成し、その中にリストコンテナを配置します。リストコンテナの高さは、スクロールバーを機能させるために、表示されるリストの長さに応じて手動で設定する必要があります(itemHeight * numberOfItems)。次に、いくつかのアイテムコンポーネントを作成して、スクロール可能なコンテナの高さを埋め、さらに1つまたは2つの模倣連続リスト効果を追加します。それらを絶対位置にし、スクロール時に位置を移動するだけで、連続リストを模倣します(実装方法を見つけると思います:)
もう1つ、DOMへの書き込みは、特に間違った場合に高価な操作です。キャンバスを使用してリストを表示し、スクロール時にスムーズなエクスペリエンスを作成できます。反応キャンバスコンポーネントをチェックアウトします。私は彼らがすでにリストのいくつかの仕事をしていると聞いた。
React Virtualized Selectをチェックしてください。この問題に対処するように設計されており、私の経験では素晴らしいパフォーマンスを発揮します。説明から:
反応仮想化および反応選択を使用してドロップダウンでオプションの大きなリストを表示するHOC
私のコメント で述べたように、ユーザーは一度にブラウザーでこれらの10000の結果をすべて必要とすることを疑います。
結果をページングし、常に10個の結果のリストを表示する場合はどうでしょう。
私は 例を作成しました Reduxのような他のライブラリを使用せずにこの手法を使用しています。現在はキーボードナビゲーションのみで使用できますが、スクロールでも機能するように簡単に拡張できます。
この例は、コンテナアプリケーション、検索コンポーネント、リストコンポーネントの3つのコンポーネントで構成されています。ほぼすべてのロジックがコンテナコンポーネントに移動されました。
要点は、start
とselected
の結果を追跡し、キーボード操作でそれらをシフトすることにあります。
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
メソッドのlimit
およびrender
に基づいて結果をスライスします。
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/69z2wepo/47841/
Reactコンポーネントにロードする前にフィルターを試して、コンポーネント内の妥当な量のアイテムのみを表示し、オンデマンドでさらにロードします。一度に多くのアイテムを表示できる人はいません。
あなたとは思わないが、キーとしてインデックスを使用しない 。
開発バージョンと製品バージョンが異なる本当の理由を見つけるには、コードをprofiling
試してみてください。
ページを読み込み、記録を開始し、変更を行い、記録を停止してから、タイミングを確認します。 Chromeでのパフォーマンスプロファイリングの手順についてはこちら を参照してください。
この問題に苦労している人のために、最大100万件のレコードまでリストを処理するコンポーネント react-big-list
を作成しました。
さらに、次のような派手な追加機能が付属しています。
かなりの数のアプリで本番環境で使用していますが、非常に効果的です。