React Developer Tools にはレンダリング(この記事では react がコンポーネントの再定義を行う処理のこと。ブラウザによる HTML と CSS を元にした画面への描画のことではありません。)が行われたコンポーネントをハイライトする仕組みがあります。これは次の様に動作します。
上動作例ではフォームの入力の度にフォーム全体が再レンダリングされていることがわかります。書き換える必要のあるコンポーネント以外も再レンダリングしているということでパフォーマンス面で不利です。特に巨大なフォームや構造体を扱う時にこれが起こるとユーザに多大なストレスをかけることになります。これを解決します。
React デフォルトのレンダリング挙動ですが大雑把にいえば”ある state や props が変化した際、それに関わる部分(主に更に値を受け渡している子コンポーネント)を全て再レンダリング”です。
React.Component – React#shouldComponentUpdateより
このためフォーム全体の値を state として持っている親コンポーネントと state と連動する各 input 要素を持つ子コンポーネントが存在する時、ある input 要素の値が変わると親コンポーネントの state が変化して連動して state に関わる子コンポーネント全てが再レンダリングされます。これが原因で入力と処理完了のタイミングの差異が無視できなくなり遅延が発生します。
解決のためにはレンダリングタイミングの制御が必要です。React が推奨する制御方法の一つは PureComponent を用いることです。
React コンポーネントの render() 関数が同じ props と state を与えられたときに同じ結果をレンダーするときは、パフォーマンスを向上させるために React.PureComponent を使用できます。
React の最上位 API – React#React.PureComponent
使い方は簡単です。普段 React.Component を extends しているところを React.PureComponent にするのみです。
// これを class Square extends React.Component { render() { return ( <button className="square" onClick={() => alert('click')}> {this.props.value} </button> ); } } // こう class Square extends React.PureComponent { render() { return ( <button className="square" onClick={() => alert('click')}> {this.props.value} </button> ); } }
これでこのコンポーネントは state や props が変化した時にのみ再レンダリングされるようになります。うまくいけば次の様に値を変化させても他のレンダリングが不要なコンポーネントが再レンダリングされないとわかります。
React のコンポーネント定義はクラスのみならず関数でもできます。関数を用いたコンポーネントで PureComponent と同様の動作をしたい場合は React.memo が使用できます。使い方は次の通り、作った関数コンポーネントを React.memo の引数にするだけです。返り値が PureComponent 同様にレンダリングタイミングが限られたコンポーネントです。
React の最上位 API – React#React.memo
const MyComponent = React.memo(function MyComponent(props) { /* render using props */ }); // こういう書き方も便利そう export const MyComponent = React.memo(MyComponentFunction) function MyComponentFunction(props) { /* render using props */ };
PureComponent, memo 両方の注意事項として浅い比較を用いて値が変化しているかを判断している点があります。このため深い部分の差異を検知できず期待した動作をしないことがあります。これが起きるのは Object, Array で内部要素の変更が重要な時です。 PureComponent や memo を用いるならば変化を検知して欲しい props や state には Object, Array を用いず String, Number, Boolean の様な型を割り当てるべきです。またこの理由により PureComponent, memo が効果的に使えるのは恐らく末端のコンポーネントです(少なくとも自分の書いたコードではそうなりました)。
比較を React の用意した浅い比較でなく独自のコードにすることもできます。
クラスの場合はライフサイクルメソッドの一つである shouldComponentUpdate を用います。
React.Component – React#shouldComponentUpdate
これを用いることで次の様に独自関数によってレンダリングの必要の有無を定義できます。
class AppInput extends Component { static propTypes = { label: PropTypes.string.isRequired, name: PropTypes.string, value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), onChange: PropTypes.func, }; /** * 現在の状態と次の状態の比較結果を bool で返す */ shouldComponentUpdate(nextProps, nextState, nextContext) { // 変化があるのは props の value のみなので value のみを比較 return this.props.value !== nextProps.value; } render() { return <label>{this.props.label}<input name={this.props.name || ''} value={value || ''} onInput={this.props.onChange} onChange={this.props.onChange} /></label>; } }
注意事項としてこのライフサイクルメソッドは非常に高頻度で呼び出される点があります。次の引用にある様に shouldComponentUpdate の中で重い処理をした場合、それはレンダリングの有無の制御による動作の軽量化よりも大きな負荷をかけることになりやすいです。
等価性を深く調べることや shouldComponentUpdate() で JSON.stringify() を使用することはおすすめしません。これは非常に非効率的であり、パフォーマンスに悪影響を及ぼします。
React.Component – React#shouldComponentUpdate
重い処理か否かの目安として React のソースコード中を当たる方法があります。
PureComponent 用の比較らしきコードが次です。それなりに複雑なので単純で部分的な比較ならばおそらくパフォーマンスが向上します(内容次第で変わるでしょうし都度実際に計測するべきです)。
// リビジョン番号 fed4ae0247883050aab6d77d9a83f9b6399d9a45 の時点の facebook/react // packages/react-reconciler/src/ReactFiberClassComponent.new.js:291 if (ctor.prototype && ctor.prototype.isPureReactComponent) { return ( !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState) ); } // packages/shared/shallowEqual.js:19 function shallowEqual(objA: mixed, objB: mixed): boolean { // ここから比較関数の中身。長いので省略
関数コンポーネントでも同様に独自の比較が可能です。次のコードにある様に React.memo の第二引数に渡した関数が独自の比較関数になります。こちらは shouldComponentUpdate と異なり true でレンダリングしない、 false でレンダリングする、と動作します。
// @see https://ja.reactjs.org/docs/react-api.html#reactmemo function MyComponent(props) { /* render using props */ } function areEqual(prevProps, nextProps) { /* nextProps を render に渡した結果が prevProps を render に渡した結果となるときに true を返し それ以外のときに false を返す */ } export default React.memo(MyComponent, areEqual);