はじめに
Reactで開発を進める中で、「想定外のタイミングでコンポーネントが再レンダリングされる」「アプリの動作が重くなる」といった問題に直面したことはありませんか? これらの原因の多くは、Reactの標準的なレンダリング動作を正しく理解していないことにあります。
この記事では、Reactのレンダリングについてまとめ、再レンダーの最適化の手段を紹介します。
Reactの標準的なレンダリング動作
Reactは、ルートコンポーネントを起点に、親から子へとコンポーネントを順番に処理し、更新が必要なコンポーネントを特定していきます。差分検出処理では、すべてのコンポーネントからアウトプットを収集した後、変更があった部分をリストアップし、それをDOMに適用していきます。
Reactの開発チームによると、このプロセスは次の2つのフェーズに分かれています。
- レンダーフェーズ:コンポーネントをレンダリングし、前回の出力との差分を計算するフェーズ
- コミットフェーズ:変更が発生した部分をDOMに適用するフェーズ
(参照:React hooks lifecycle diagram)
このようにReactは、まずコンポーネントのレンダリング結果を計算し、それから実際にDOMへ変更を適用する、という2段階のプロセスで画面を更新します。
また、Reactの標準的なレンダリング動作として、親コンポーネントが再レンダーされると、配下の子コンポーネントも再帰的に再レンダーされるようになっています。
例えば、ルートの<App>
コンポーネントでsetState()
を呼び出すと、すべてのコンポーネントが再レンダリングされます。
多くの場合、大部分のコンポーネントは前回と同じ出力を生成するため、実際のDOM更新は少なくて済みますが、Reactはすべてのコンポーネントの出力を検証して差分を確認するため、その分の処理コストがかかります。
注意すべき点として、コンポーネントが再レンダリングされたからといって、必ずしもDOMが更新されるわけではありません。コンポーネントから返される要素ツリーが前回と同じであれば、Reactは変更なしと判断し、DOM更新をスキップします。
差分検出処理とコンポーネントタイプ
Reactは再レンダリングを効率化するために、可能な限り既存のコンポーネントツリーとDOM構造を再利用します。コンポーネントのタイプや表示位置が同じであれば、そのコンポーネントインスタンスは保持されます。
レンダリングのプロセスでは、要素のtypeフィールドを基準に比較を行います。コンポーネントタイプが変更された場合、Reactはそのコンポーネントツリー全体が変更されたと判断し、該当部分のコンポーネントツリーとDOMノードを完全に破棄して新しく作り直します。
このような破壊と再作成のプロセスが発生するため、レンダリング中に新しいコンポーネント型を動的に生成することは避けるべきです。そうしないと、パフォーマンスが大幅に低下する可能性があります。
NG
// ❌ BAD!
// This creates a new `ChildComponent` reference every time!
function ParentComponent() {
function ChildComponent() {
return <div>Hi</div>;
}
return <ChildComponent />;
}
OK
// ✅ GOOD
// This only creates one component type reference
function ChildComponent() {
return <div>Hi</div>;
}
function ParentComponent() {
return <ChildComponent />;
}
差分検出処理とkey
React はkey
の属性を参考にしてコンポーネントのインスタンスを認識します。
<SomeComponent key=”someValue”>
実際にはpropではなく識別子として使われ、React側ではkey
を除外するので、props.key
は存在しません;いつもundefined
になります。
key
の主な用途はリストレンダリング時のアイテム識別です。リストアイテムの区別や、可変データを効率的にレンダリングするために重要な役割を果たします。Reactでは、配列のインデックスではなく、データから抽出したユニークな値をkeyとして使用することが推奨されています。
// ✅ Use a data object ID as the key for list items
todos.map((todo) => <TodoListItem key={todo.id} todo={todo} />);
その理由を具体例で説明します。10個のアイテムからなるリストがあり、6番目と7番目のアイテムを削除し、最後に3つのアイテムを追加するケースを考えてみましょう。インデックスをkeyとして使用していた場合、Reactの視点からは「1つのアイテムだけが追加された」ように見えてしまいます。
その結果、既存のDOMノードとコンポーネントインスタンスが再利用されますが、データの順序が変わっているため、コンポーネントが表示するデータも変わり、予期しない動作を引き起こす可能性があります。
一方、key={todo.id}
のようにデータ固有のIDを使用していた場合、Reactの観点からは「2個のアイテムが削除され、3個の新しいアイテムが追加された」と認識します。これにより、削除されたインスタンスは破棄され、新しいアイテム用に3つの新しいコンポーネントインスタンスが作成されます。変更されていないコンポーネントは不必要に更新されなくなるため、パフォーマンスも最適化されます。
key
の属性はリスト要素だけでなく、どのコンポーネントにも適用できて、コンポーネントの削除・再作成の合図として利用できます。key
の属性を変更すると、古いコンポーネントインスタンスとして認識されるので、Reactはそのコンポーネントを再作成します。
まとめ
今回はReactのレンダリングの仕組みと最適化のポイントについて解説しました。ぜひ参考にしてみてください。
弊社オウンドメディアin-PocketではReact初学者向けの情報も発信しています。こちらもあわせてご覧ください。
> React初学者が必ず押さえておきたい考え方とは?【コンポーネント指向のフロントエンド】