この記事では、React コードを書くためのいくつかの提案をまとめており、React のレンダリングメカニズムを理解するのに役立ちます。主に不必要な再レンダリングを減らすことで React のパフォーマンスを向上させることを目的としています。
React を使用して開発した経験を振り返ると、もうすぐ 2 年になります。高性能の React コードを書く方法やパフォーマンスの最適化についての記事を書きたいと思っていましたので、高性能の React コードを書くための実践をまとめました。これがあなたの React の理解を深める手助けになるかもしれません。
React のパフォーマンスに影響を与える主な要因#
React はユーザーインターフェースの構築に特化しており、内部で VirtualDOM と Diff アルゴリズムを使用してビューの更新を実現しています。元々高いパフォーマンスを持っています。コンポーネントの state や props が変更されると、React は再レンダリングを行い、ビューを更新します。レンダリングプロセスでは、React は JSX 構文を仮想 DOM に変換し、Diff アルゴリズムを使用して新旧の DOM の変化を比較し、ビューを部分的に更新します。想像してみてください。レンダリングが実行された後、JSX が仮想 DOM に変換され、Diff が行われ、最終的に新旧の DOM が同じであることが判明した場合、ビューは更新されません。これにより、不必要なレンダリングが発生し、パフォーマンスの浪費が生じます。したがって、不必要なレンダリングを減らすことが React のパフォーマンスを最適化する主な要因となります。
state はビューにのみ関連する#
React コンポーネントを書くとき、変数を使用することは間違いありません。変数は this に保存することも、state に保存することも、グローバル変数を使用することもできます。では、いつ state を使用すべきでしょうか?答えは、ビューに関連する変数のみを state に置き、ビューに関連しないものは this に置くべきです。ビューの更新に関係のない変数を state に置き、その変数が頻繁に更新される場合、毎回更新するたびに再レンダリングが必要になり、パフォーマンスの浪費を引き起こします。this を使用して保存する方が明らかに良い選択です。
shouldComponentUpdate#
以下のコードを考えてみましょう:
export default class Home extends Component {
state = {
count: 0
}
add = () => {
this.setState({count: 0});
}
render() {
console.log('render');
return (
<div className="App">
<div onClick={this.add}>+++</div>
<p className="App-intro">
{this.state.count}
</p>
</div>
);
}
}
+++ をクリックして add がトリガーされると、setState が state を更新しますが、新旧の state は全く同じです。React は Component コンポーネント内で最適化を行わず、不必要なレンダリングが実行されます。この場合、手動で最適化を行う必要があります。state または props が更新された後、React コンポーネントのライフサイクル shouldComponentUpdate がトリガーされ、shouldComponentUpdate 内で新旧の state と props を比較し、コンポーネントが再レンダリングする必要があるかどうかを判断できます。
shouldComponentUpdate(nextProps, nextState) {
if(nextState.count === this.state.count) {
return false;
}
}
デフォルトでは shouldComponentUpdate は true を返し、常に再レンダリングされます。この場合、shouldComponentUpdate を使用して比較した後、false を返すと、React は再レンダリングを行いません。
state が文字列などの基本型の場合、shouldComponentUpdate は問題なく対応できますが、state がオブジェクトや配列などの参照型の場合はどうでしょうか?参照型の場合は、再帰的に比較する必要があります。または、JSON.stringify (nextState.obj) === JSON.stringify (this.state.obj) を使用して比較することもできますが、オブジェクトが深くネストされている場合や関数、正規表現などの型の場合、shouldComponentUpdate はその効果を失います。
PureComponent#
React.PureComponent は React.Component とほぼ完全に同じですが、React.PureComponent は prop と state の浅い比較を通じて shouldComponentUpdate () を実現します。簡単に言えば、PureComponent は内部で shouldComponentUpdate を実装しており、私たちが比較コードを書く必要はありません。React.PureComponent の shouldComponentUpdate () はオブジェクトに対してのみ浅い比較を行います。オブジェクトが複雑なデータ構造を含む場合、深層のデータの不一致により誤った否定判断を引き起こす可能性があります(オブジェクトの深層データが変更されてもビューが更新されないことを示します)。テストによると、PureComponent を使用することで shouldComponentUpdate よりもわずかにパフォーマンスが向上します。
StateLessComponent#
無状態コンポーネント(StateLessComponent)は関数型コンポーネントとも呼ばれます。
const Text = ({ children = 'Hello World!' }) =>
<p>{children}</p>
関数型コンポーネントは class 宣言がなく、ライフサイクルもなく、render メソッドのみを使用して実装され、コンポーネントのインスタンス化プロセスが存在しないため、余分なメモリを割り当てる必要がなく、レンダリングパフォーマンスが向上します。
アロー関数と bind#
コンポーネントにイベントハンドラをバインドする際、state に正常にアクセスできるようにするために、通常は bind またはアロー関数を使用します。
<Button onClick={this.update.bind(this)} />
または
<Button onClick={() => { console.log("Click"); }} />
上記の状況では、bind は毎回新しい関数を返し、アロー関数は毎回 render 時に再割り当てされます(bind を使用する方法と同様です)。コンポーネントにとって、毎回バインドされるのは新しい関数であり、Button コンポーネントが props を比較する際に前後の props が異なると見なされ、再レンダリングが発生します。推奨される書き方は 2 つあります:
update = () => {
//
}
<Button onClick={this.update} />
または
constructor(props) {
super(props);
this.update = this.update.bind(this);
}
update() {
//
}
<Button onClick={this.update} />
インラインスタイル#
コンポーネントにスタイルを設定する際、次のようなコードを書くことがあります:
<Button style={{width: 100, marginTop: 50 }} />
この書き方では、毎回 render 時に style が新しいオブジェクトを返します。参照型は参照アドレスを比較するため、毎回の結果は不等しくなり、再レンダリングを引き起こします。推奨される書き方は className を使用することです。
<Button className="btn" />
属性の渡し方と []#
コンポーネントにデフォルト値を設定する際:
<RadioGroup options={this.props.options || []} />
もし毎回 this.props.options の値が null であれば、に渡されるのは常にリテラル配列 [] であることを意味しますが、リテラル配列と new Array () の効果は同じで、常に新しいインスタンスが生成されます。したがって、見た目上は毎回コンポーネントに渡されるのは同じ空の配列ですが、実際にはコンポーネントにとっては毎回新しい属性となり、レンダリングを引き起こします。したがって、一般的な値を変数として保存するのが正しい方法です:
const DEFAULT_OPTIONS = [];
<RadioGroup options={this.props.options || DEFAULT_OPTIONS} />
map と key#
React では、リストをレンダリングするために map を頻繁に使用するでしょう。各リスト要素に key 属性を設定し忘れると、React はコンソールに「リストアイテムには key を提供する必要があります」と警告します。実際、Keys は DOM 内の特定の要素が追加または削除されるときに、React がどの要素が変更されたかを識別するのに役立ちます。したがって、配列内の各要素に確定的な識別子を与えるべきです。
要素の key は、その要素がリスト内で持つユニークな文字列が最適です。通常、データからの id を要素の key として使用します:
const todoItems = todos.map((todo) =>
<li key={todo.id}>
{todo.text}
</li>
);
要素に確定的な id がない場合、その序列番号インデックスを key として使用できます:
const todoItems = todos.map((todo, index) =>
// アイテムに安定したIDがない場合のみ行います
<li key={index}>
{todo.text}
</li>
);
todoItems 内のデータが変更されると、インデックスを key として使用したリストはすべて再レンダリングされます。したがって、ソートにインデックスを使用することはお勧めできません。なぜなら、これによりレンダリングが非常に遅くなるからです。
render 内での変数の実行を減らす#
コンポーネントの state や props が変更されると、render が実行されることがわかっています。したがって、render 内で変数や関数を実行すると、レンダリングパフォーマンスに影響を与える可能性があるため、render 内での変数や関数の実行を減らすべきです。
props と state の簡素化#
上記からわかるように、props と state が参照型の場合、React 内部では props と state の浅い比較のみが行われます。したがって、props と state はできるだけ基本データ型に設定し、オブジェクトのネストレベルを深くしすぎず、props のパラメータ数を減らし、コンポーネントの props と state オブジェクトのデータをできるだけフラットにすることで、React のレンダリングパフォーマンスを最適化できる可能性があります。
まとめ#
この記事では、React コードを書くための一連の原則について議論しました。主に再レンダリングを減らすことで React コードのパフォーマンスを向上させることを目的としています。パフォーマンスの最適化は常に持続的なテーマであり、immutable.js のような不変データ構造を使用したり、redux 内の複雑な計算をキャッシュするために reselect を使用したり、コンポーネントをできるだけ子コンポーネントに分割することなど、さまざまな観点から考慮することができます。次回お会いしましょう。