レンダリング、コミット、マウント
このドキュメントは、現在展開中の新しいアーキテクチャに関するものです。
React Native レンダラーは、React ロジックをホストプラットフォームにレンダリングするための一連の作業を実行します。この一連の作業はレンダリングパイプラインと呼ばれ、初期レンダリングと UI 状態の更新に対して発生します。このドキュメントでは、レンダリングパイプラインと、これらのシナリオでの違いについて説明します。
レンダリングパイプラインは、大きく3つのフェーズに分けられます。
- レンダリング: React はプロダクトロジックを実行し、JavaScript でReact エレメントツリーを作成します。このツリーから、レンダラーは C++ でReact シャドウツリーを作成します。
- コミット: React シャドウツリーが完全に作成された後、レンダラーはコミットをトリガーします。これにより、React エレメントツリーと新しく作成された React シャドウツリーの両方が、マウントされる「次のツリー」として昇格されます。また、レイアウト情報の計算もスケジュールされます。
- マウント: レイアウト計算の結果を含む React シャドウツリーは、ホストビューツリーに変換されます。
レンダリングパイプラインのフェーズは、異なるスレッドで発生する可能性があります。詳細については、スレッドモデルのドキュメントを参照してください。

初期レンダリング
次のようにレンダリングしたいとします。
function MyComponent() {
return (
<View>
<Text>Hello, World</Text>
</View>
);
}
// <MyComponent />
上記の例では、<MyComponent /> はReact エレメントです。React は、この*React エレメント*を、それ以上縮小できなくなるまで(または JavaScript クラスで実装されている場合はそのrenderメソッドを)呼び出すことで、末端のReact ホストコンポーネントに再帰的に縮小します。これで、React ホストコンポーネントの*React エレメントツリー*ができました。
フェーズ1. レンダリング

この要素縮小のプロセス中、各*React エレメント*が呼び出されるたびに、レンダラーも同期的にReact シャドウノードを作成します。これは*React ホストコンポーネント*の場合にのみ発生し、React 複合コンポーネントでは発生しません。上記の例では、<View> は ViewShadowNode オブジェクトの作成につながり、<Text> は TextShadowNode オブジェクトの作成につながります。注目すべきは、<MyComponent> を直接表す*React シャドウノード*は決して存在しないということです。
React が2つの*React Element Node*間に親子関係を作成するたびに、レンダラーは対応する*React Shadow Node*間に同じ関係を作成します。これが*React Shadow Tree*が組み立てられる方法です。
追加の詳細
- これらの操作(*React Shadow Node*の作成、2つの*React Shadow Node*間の親子関係の作成)は同期的なスレッドセーフな操作であり、通常は JavaScript スレッド上で React (JavaScript) からレンダラー (C++) に実行されます。
- *React Element Tree*(およびその構成要素である*React Element Node*)は、無期限に存在するわけではありません。それは、React の「ファイバー」によって具体化された一時的な表現です。ホストコンポーネントを表す各「ファイバー」は、JSI によって可能になった*React Shadow Node*への C++ ポインタを格納します。このドキュメントで「ファイバー」について詳しく学んでください。
- *React シャドウツリー*は不変です。*React シャドウノード*を更新するには、レンダラーは新しい*React シャドウツリー*を作成します。ただし、レンダラーは状態更新のパフォーマンスを向上させるためにクローン操作を提供しています(詳細についてはReact 状態更新を参照)。
上記の例では、レンダリングフェーズの結果はこのようになります。

*React シャドウツリー*が完了した後、レンダラーは*React エレメントツリー*のコミットをトリガーします。
フェーズ2. コミット

コミットフェーズは、*レイアウト計算*と*ツリー昇格*の2つの操作で構成されます。
- レイアウト計算: この操作は、各*React Shadow Node*の位置とサイズを計算します。React Nativeでは、これはYogaを呼び出して各*React Shadow Node*のレイアウトを計算することを含みます。実際の計算には、JavaScriptの*React Element*に由来する各*React Shadow Node*のスタイルが必要です。また、*React Shadow Tree*のルートのレイアウト制約も必要で、これは結果のノードが占有できる利用可能なスペースの量を決定します。

- ツリー昇格 (新規ツリー → 次のツリー): この操作は、新しい*React シャドウツリー*をマウントされる「次のツリー」として昇格させます。この昇格は、新しい*React シャドウツリー*がマウントに必要なすべての情報を持っており、*React エレメントツリー*の最新の状態を表していることを示します。「次のツリー」は、UI スレッドの次の「ティック」でマウントされます。
追加の詳細
- これらの操作は、バックグラウンドスレッドで非同期に実行されます。
- レイアウト計算の大部分は C++ 内で完全に実行されます。ただし、一部のコンポーネント(例: `Text`、`TextInput` など)のレイアウト計算は、*ホストプラットフォーム*に依存します。テキストのサイズと位置は各*ホストプラットフォーム*に固有であり、*ホストプラットフォーム*層で計算する必要があります。この目的のために、Yoga は*ホストプラットフォーム*で定義された関数を呼び出して、コンポーネントのレイアウトを計算します。
フェーズ3. マウント

マウントフェーズでは、*React シャドウツリー*(レイアウト計算からのデータを含む)を、画面上にピクセルがレンダリングされた*ホストビューツリー*に変換します。復習として、*React エレメントツリー*はこのようになります。
<View>
<Text>Hello, World</Text>
</View>
高レベルでは、React Native レンダラーは各*React Shadow Node*に対応するホストビューを作成し、画面にマウントします。上記の例では、レンダラーは<View>用にandroid.view.ViewGroupのインスタンスを、<Text>用にandroid.widget.TextViewを作成し、「Hello World」を格納します。同様に、iOSではUIViewが作成され、NSLayoutManagerへの呼び出しでテキストが格納されます。次に、各ホストビューは、そのReact Shadow Nodeからのプロップを使用するように構成され、そのサイズと位置は計算されたレイアウト情報を使用して構成されます。

より詳しく見ると、マウントフェーズは以下の3つのステップで構成されます。
- ツリーの差分検出: このステップでは、「以前にレンダリングされたツリー」と「次のツリー」間の差分を C++ で完全に計算します。結果は、ホストビューに対して実行される原子的なミューテーション操作のリスト(例: `createView`、`updateView`、`removeView`、`deleteView` など)です。このステップでは、不必要なホストビューの作成を避けるために React シャドウツリーがフラット化されます。このアルゴリズムの詳細については、ビューのフラット化を参照してください。
- ツリー昇格 (次のツリー → レンダリングされたツリー): このステップでは、「次のツリー」を「以前にレンダリングされたツリー」に原子的に昇格させ、次のマウントフェーズで適切なツリーとの差分を計算できるようにします。
- ビューマウント: このステップでは、原子的なミューテーション操作を対応するホストビューに適用します。このステップは、*ホストプラットフォーム*の UI スレッドで実行されます。
追加の詳細
- これらの操作はUIスレッド上で同期的に実行されます。コミットフェーズがバックグラウンドスレッドで実行される場合、マウントフェーズはUIスレッドの次の「ティック」でスケジュールされます。一方、コミットフェーズがUIスレッドで実行される場合、マウントフェーズは同じスレッドで同期的に実行されます。
- マウントフェーズのスケジューリング、実装、実行は、*ホストプラットフォーム*に大きく依存します。例えば、現在、マウント層のレンダラーアーキテクチャは Android と iOS で異なります。
- 初期レンダリング時には、「以前にレンダリングされたツリー」は空です。そのため、ツリーの差分計算ステップでは、ビューの作成、プロップの設定、およびビュー同士の追加のみで構成されるミューテーション操作のリストが生成されます。ツリーの差分計算は、React の状態更新を処理する際にパフォーマンスにとってより重要になります。
- 現在のプロダクションテストでは、*React Shadow Tree* は通常約 600 ~ 1000 の*React Shadow Node* (ビューのフラット化前) で構成され、ビューのフラット化後は約 200 ノードに削減されます。iPad やデスクトップアプリでは、この量は 10 倍に増加する可能性があります。
React の状態更新
React Element Tree の状態が更新されたときのレンダリングパイプラインの各フェーズを見ていきましょう。初期レンダリングで次のコンポーネントをレンダリングしたとします。
function MyComponent() {
return (
<View>
<View
style={{backgroundColor: 'red', height: 20, width: 20}}
/>
<View
style={{backgroundColor: 'blue', height: 20, width: 20}}
/>
</View>
);
}
初期レンダリングセクションで説明したことを適用すると、次のツリーが作成されると予想されます。

ノード3が赤い背景のホストビューにマッピングされ、ノード4が青い背景のホストビューにマッピングされていることに注目してください。JavaScript プロダクトロジックの状態更新の結果として、最初のネストされた <View> の背景が 'red' から 'yellow' に変更されたと仮定します。新しい*React エレメントツリー*はこのようになるでしょう。
<View>
<View
style={{backgroundColor: 'yellow', height: 20, width: 20}}
/>
<View
style={{backgroundColor: 'blue', height: 20, width: 20}}
/>
</View>
この更新は React Native によってどのように処理されるのでしょうか?
状態更新が発生すると、レンダラーはすでにマウントされているホストビューを更新するために、概念的に*React エレメントツリー*を更新する必要があります。しかし、スレッドセーフティを維持するために、*React エレメントツリー*と*React シャドウツリー*の両方は不変でなければなりません。これは、現在の*React エレメントツリー*と*React シャドウツリー*を変更する代わりに、React は新しいプロップ、スタイル、および子を組み込んだ各ツリーの新しいコピーを作成する必要があることを意味します。
状態更新中のレンダリングパイプラインの各フェーズを見ていきましょう。
フェーズ1. レンダリング

React が新しい状態を組み込んだ新しい*React Element Tree*を作成するとき、変更の影響を受けるすべての*React Element*と*React Shadow Node*をクローンする必要があります。クローン後、新しい*React Shadow Tree*がコミットされます。
React Native レンダラーは、不変性のオーバーヘッドを最小限に抑えるために構造的共有を利用します。新しい状態を含めるために*React Element*がクローンされると、ルートまでのパス上にあるすべての*React Element*がクローンされます。React は、プロップ、スタイル、または子に対する更新が必要な場合にのみ React Element をクローンします。 状態更新によって変更されない*React Element*は、古いツリーと新しいツリーで共有されます。
上記の例では、React は以下の操作を使用して新しいツリーを作成します。
- CloneNode(ノード 3,
{backgroundColor: 'yellow'}) → ノード 3' - CloneNode(ノード 2) → ノード 2'
- AppendChild(ノード 2', ノード 3')
- AppendChild(ノード 2', ノード 4)
- CloneNode(ノード 1) → ノード 1'
- AppendChild(ノード 1', ノード 2')
これらの操作の後、Node 1' は新しい*React エレメントツリー*のルートを表します。「以前にレンダリングされたツリー」をT、「新しいツリー」をT'としましょう。

T と T' の両方が ノード 4 を共有していることに注目してください。構造的な共有はパフォーマンスを向上させ、メモリ使用量を削減します。
フェーズ2. コミット

React が新しい*React Element Tree*と*React Shadow Tree*を作成した後、それらをコミットする必要があります。
- レイアウト計算: 初期レンダリング時のレイアウト計算と同様です。重要な違いの1つは、レイアウト計算が共有されている*React Shadow Node*のクローン作成を引き起こす可能性があることです。これは、共有されている*React Shadow Node*の親がレイアウト変更を受けると、共有されている*React Shadow Node*のレイアウトも変更される可能性があるためです。
- ツリー昇格 (新しいツリー → 次のツリー): 初期レンダリング時のツリー昇格と同様。
フェーズ3. マウント

- ツリー昇格 (次のツリー → レンダリングされたツリー): このステップでは、「次のツリー」を「以前にレンダリングされたツリー」に原子的に昇格させ、次のマウントフェーズで適切なツリーとの差分を計算できるようにします。
- ツリーの差分検出: このステップでは、「以前にレンダリングされたツリー」(T)と「次のツリー」(T')の差分を計算します。結果は、*ホストビュー*に対して実行される原子的なミューテーション操作のリストです。
- 上記の例では、操作は次のようになります。 `UpdateView(**Node 3**, {backgroundColor: 'yellow'})`
- 差分は、現在マウントされている任意のツリーと新しいツリー間で計算できます。レンダラーはツリーの中間バージョンをスキップできます。
- ビューマウント: このステップでは、原子的なミューテーション操作を対応する*ホストビュー*に適用します。上記の例では、ビュー3の
backgroundColorのみが更新されます(黄色に)。

React Native レンダラーの状態更新
*シャドウツリー*内のほとんどの情報について、React が唯一の所有者であり、信頼できる唯一の情報源です。すべてのデータは React から発信され、データの流れは一方向です。
ただし、1つの例外と重要なメカニズムがあります。C++ のコンポーネントは、JavaScript に直接公開されていない状態を含むことができ、JavaScript は信頼できる情報源ではありません。C++ と*ホストプラットフォーム*がこの*C++ 状態*を制御します。一般的に、これは*C++ 状態*を必要とする複雑な*ホストコンポーネント*を開発している場合にのみ関係します。*ホストコンポーネント*の大多数は、この機能を必要としません。
例えば、`ScrollView` はこのメカニズムを使用して、レンダラーに現在のオフセットを知らせます。この更新は*ホストプラットフォーム*、具体的には`ScrollView`コンポーネントを表すホストビューからトリガーされます。オフセットに関する情報は、measureのようなAPIで使用されます。この更新はホストプラットフォームから発生し、React Element Treeには影響しないため、この状態データは*C++ State*によって保持されます。
概念的に、*C++ 状態*の更新は、上記のReact 状態更新と同様です。ただし、2つの重要な違いがあります。
- React が関与しないため、「レンダリングフェーズ」はスキップされます。
- 更新は、メインスレッドを含むあらゆるスレッドで発生し、実行される可能性があります。
フェーズ2. コミット

*C++ 状態*の更新を実行する際、あるコードブロックが`ShadowNode` (N) の更新を要求し、*C++ 状態*を値Sに設定します。React Native レンダラーは、Nの最新コミット済みバージョンを取得し、新しい状態Sでクローンを作成し、N’をツリーにコミットすることを繰り返し試みます。この間にReact、または別の*C++ 状態*の更新が別のコミットを実行した場合、*C++ 状態*のコミットは失敗し、レンダラーはコミットが成功するまで*C++ 状態*の更新を何度も再試行します。これにより、信頼できる情報源の衝突や競合が防止されます。
フェーズ3. マウント

*マウントフェーズ*は、React 状態更新のマウントフェーズとほぼ同じです。レンダラーは依然としてレイアウトを再計算し、ツリーの差分を計算する必要があります。詳細については上記のセクションを参照してください。