Reactアプリケーションを最適化して、要素のリストを表示します

ページ上の要素のリスト(セット)の表示は、ほとんどすべてのWebアプリケーションの標準タスクです。 この投稿では、パフォーマンスを改善するためのいくつかのヒントを共有したいと思います。



テストケースの場合、キャンバス要素に多くの「目標」(円)を描く小さなアプリケーションを作成します。 データストアとしてreduxを使用しますが、これらのヒントは、状態を保存する他の多くの方法で機能します。

これらの最適化はreact-reduxでも適用できますが、説明を簡単にするためにこのライブラリは使用しません。



これらのヒントは、アプリケーションのパフォーマンスを20倍改善できます。







状態の説明から始めましょう。



function generateTargets() { return _.times(1000, (i) => { return { id: i, x: Math.random() * window.innerWidth, y: Math.random() * window.innerHeight, radius: 2 + Math.random() * 5, color: Konva.Util.getRandomColor() }; }); } //       //    "UPDATE",     function appReducer(state, action) { if (action.type === 'UPDATE') { const i = _.findIndex(state.targets, (t) => t.id === action.id); const updatedTarget = { ...state.targets[i], radius: action.radius }; state = { targets: [ ...state.targets.slice(0, i), updatedTarget, ...state.targets.slice(i + 1) ] } } return state; } const initialState = { targets: generateTargets() }; //   const store = Redux.createStore(appReducer, initialState);
      
      







次に、アプリケーションの図面を作成します。 react-konvaを使用してキャンバスに描画します。



 function Target(props) { const {x, y, color, radius} = props.target; return ( <Group x={x} y={y}> <Circle radius={radius} fill={color} /> <Circle radius={radius * 1 / 2} fill="black" /> <Circle radius={radius * 1 / 4} fill="white" /> </Group> ); } //      class App extends React.Component { constructor(...args) { super(...args); this.state = store.getState(); // subscibe to all state updates store.subscribe(() => { this.setState(store.getState()); }); } render() { const targets = this.state.targets.map((target) => { return <Target key={target.id} target={target}/>; }); const width = window.innerWidth; const height = window.innerHeight; return ( <Stage width={width} height={height}> <Layer hitGraphEnabled={false}> {targets} </Layer> </Stage> ); } }
      
      







完全なデモ: http : //codepen.io/lavrton/pen/GZXzGm



次に、1つの「目標」を更新する簡単なテストを作成しましょう。



 const N_OF_RUNS = 500; const start = performance.now(); _.times(N_OF_RUNS, () => { const id = 1; let oldRadius = store.getState().targets[id].radius; //  redux  store.dispatch({type: 'UPDATE', id, radius: oldRadius + 0.5}); }); const end = performance.now(); console.log('sum time', end - start); console.log('average time', (end - start) / N_OF_RUNS);
      
      







ここで、最適化なしでテストを実行します。 私のマシンでは、1回の更新に約21msかかります。



画像



今回は、キャンバス要素に描画するプロセスは含まれていません。 react-konvaはアニメーションの次のティックでのみ(非同期に)キャンバスに描画するため、reactおよびreduxコードのみ。 ここで、キャンバスでのペイントの最適化を検討しません。 これは別の記事のトピックです。



したがって、1000個の要素に対して21ミリ秒が非常に良いパフォーマンスです。 要素の更新がめったに行われない場合は、このコードをそのままにしておくことができます。



しかし、非常に頻繁に要素を更新する必要がある状況がありました(ドラッグアンドドロップ中のマウスの各移動で)。 60FPSを取得するには、1回の更新に16ミリ秒しかかからないことが必要です。 したがって、21msはそれほど大きくありません(キャンバスへの描画は後で行われることに注意してください)。



それで、何ができるのでしょうか?



1.変更されていないアイテムを更新しないでください





これは、パフォーマンスを改善するための最初の明白なルールです。 必要なのは、 ターゲットコンポーネントにshouldComponentUpdateを実装することだけです。



 class Target extends React.Component { shouldComponentUpdate(newProps) { return this.props.target !== newProps.target; } render() { const {x, y, color, radius} = this.props.target; return ( <Group x={x} y={y}> <Circle radius={radius} fill={color} /> <Circle radius={radius * 1 / 2} fill="black" /> <Circle radius={radius * 1 / 4} fill="white" /> </Group> ); } }
      
      







そのような追加の結果( http://codepen.io/lavrton/pen/XdPGqj ):



画像



いいね! 4msは21msよりもはるかに優れています。 しかし、それは可能ですか? 私の実際のアプリケーションでは、そのような最適化の後でも、パフォーマンスはあまり良くありませんでした。



Appコンポーネントのレンダリング機能を見てください。 私が本当に好きではないことは、 レンダリング関数のコードが更新のたびに実行されることです。 つまり、「ターゲット」ごとに1000個のReact.createElementが削除されます。 この例では、これはすぐに機能しますが、実際のアプリケーションではすべてが悲しくなります。



1つのアイテムのみが更新されていることがわかっている場合、リスト全体を再描画する必要があるのはなぜですか この単一のアイテムを直接更新することは可能ですか?



2子供を「賢く」する





アイデアは非常に簡単です。



1.リストの要素数が同じで、順序が変更されていない場合は、Appコンポーネントを更新しないでください。



2.データが変更された場合、子供は自分で更新する必要があります。



そのため、 ターゲットコンポーネントは状態の変更をリッスンし、変更を適用する必要があります。



 class Target extends React.Component { constructor(...args) { super(...args); this.state = { target: store.getState().targets[this.props.index] }; // subscibe to all state updates this.unsubscribe = store.subscribe(() => { const newTarget = store.getState().targets[this.props.index]; if (newTarget !== this.state.target) { this.setState({ target: newTarget }); } }); } shouldComponentUpdate(newProps, newState) { return this.state.target !== newState.target; } componentWillUnmount() { this.unsubscribe(); } render() { const {x, y, color, radius} = this.state.target; return ( <Group x={x} y={y}> <Circle radius={radius} fill={color} /> <Circle radius={radius * 1 / 2} fill="black" /> <Circle radius={radius * 1 / 4} fill="white" /> </Group> ); } }
      
      







AppコンポーネントのshouldComponentUpdateも実装する必要があります。



 shouldComponentUpdate(newProps, newState) { //     -    //    id  ,     ""  const changed = newState.targets.find((target, i) => { return this.state.targets[i].id !== target.id; }); return changed; }
      
      







これらの変更後の結果( http://codepen.io/lavrton/pen/bpxZjy ):



画像



更新ごとに0.25msがすでにはるかに優れています。



ボーナスチップ





https://github.com/mobxjs/mobxを使用して、これらすべてのサブスクリプションのコードを変更およびチェック用に作成しないでくださいmobxhttp://codepen.io/lavrton/pen/WwPaeV )のみを使用して記述された同じアプリケーション:



画像



以前の結果よりも約1.5倍高速に動作します(要素の数が多いほど、違いは顕著になります)。 そして、コードははるかに簡単です:



 const {Stage, Layer, Circle, Group} = ReactKonva; const {observable, computed} = mobx; const {observer} = mobxReact; class TargetModel { id = Math.random(); @observable x = 0; @observable y = 0; @observable radius = 0; @observable color = null; constructor(attrs) { _.assign(this, attrs); } } class State { @observable targets = []; } function generateTargets() { _.times(1000, (i) => { state.targets.push(new TargetModel({ id: i, x: Math.random() * window.innerWidth, y: Math.random() * window.innerHeight, radius: 2 + Math.random() * 5, color: Konva.Util.getRandomColor() })); }); } const state = new State(); generateTargets(); @observer class Target extends React.Component { render() { const {x, y, color, radius} = this.props.target; return ( <Group x={x} y={y}> <Circle radius={radius} fill={color} /> <Circle radius={radius * 1 / 2} fill="black" /> <Circle radius={radius * 1 / 4} fill="white" /> </Group> ); } } @observer class App extends React.Component { render() { const targets = state.targets.map((target) => { return <Target key={target.id} target={target}/>; }); const width = window.innerWidth; const height = window.innerHeight; return ( <Stage width={width} height={height}> <Layer hitGraphEnabled={false}> {targets} </Layer> </Stage> ); } } ReactDOM.render( <App/>, document.getElementById('container') );
      
      






All Articles