グローバルナビゲーションへ

本文へ

フッターへ

お役立ち情報Blog



Reactのレンダリング、差分検出処理の仕組みを学ぶ

恥ずかしながらReactのレンダリングとは何か、差分検出処理とは何かをよく分からずに、 再レンダリングを回避するため雰囲気でReact.memoを使ってきました。

今回はReactのレンダリングと差分検出処理について調べました。

Reactの差分検出処理とは

まずは公式ドキュメントの差分検出処理についてまとめます。

異なる型の要素の場合

  • ルート要素の型が違う場合、Reactは古いツリーを破棄して新しいツリーをゼロから構築する
  • ツリーを破棄するタイミングで古いDOMノードは破棄される
  • 配下のコンポーネントはアンマウントされ、アンマウントされたコンポーネントのstateも破棄される
  • 古いDOMノードが破棄される時に、useEffectで定義したクリーンアップ関数を実行する(クラスコンポーネントのcomponentWillUnmount)
  • 新しいツリーを構築する時に、新しいDOMノードがDOMに挿入される
  • DOMに挿入された後、useLayoutEffect, useEffectで定義した関数を実行する(クラスコンポーネントのcomponentDidMount)

注:ここで言われている型とは、 <div> ,  <span> ,  <Counter />  のような要素自体のことを差していると思われます

同じ型の要素の場合

  • 同じ型のReact DOM要素の場合、共通のDOMノードを保持したまま変更された属性のみを更新する

注:ここで言われているReact DOM要素とは <div /> などのJSX式を差していると思われます。

同じ型のコンポーネント要素の場合

  • 同じ型のコンポーネントが更新される場合、インスタンスは同じままpropsを更新する
  • propsを更新した後、useEffectで定義した関数を実行する(クラスコンポーネントのcomponentDidUpdate)
  • render(関数コンポーネントならコンポーネント関数)を実行する

子要素の再帰的な処理

  • DOMノードの子要素を再帰的に処理する場合、Reactは子要素リストの最初から比較していき、差分を見つけたところから更新をする
  • デフォルトでは子要素のリストにappendする場合は元の子要素リストのDOMノードは保持されるが、prependの場合は全ての子要素を変更する
  • この問題に対応するためReactはkey属性を受け取り、keyが同じ子要素は移動されたものとしてReactが判断できるようになる(結果必要のない更新を抑止できる)

レンダリングプロセス

公式ドキュメントの内容だけではまだよく理解できなかったので、Reduxのメンテナーのレンダリングの動作に関する記事を参考にもう少し詳細をまとめます。

レンダリングプロセスは、概念的に2つのフェーズに分割されます。

レンダーフェーズ コンポーネントのレンダリングと変更の計算(Reconciliation)
純粋で副作用を持たないフェーズ
コミットフェーズ 変更をDOMに適用するプロセス
副作用があり、更新のタイミングを決めるフェーズ

レンダーフェーズ

レンダーフェーズではコンポーネントツリーのルートから子要素に向かって、更新が必要であるとフラグをつけられた全てのコンポーネントを探します。
フラグがつけられたコンポーネント毎にコンポーネントのrender(関数コンポーネントの場合はコンポーネントの関数)を実行します。

// This JSX syntax:
return <SomeComponent a={42} b="testing">Text here</SomeComponent>

// is converted to this call:
return React.createElement(SomeComponent, {a: 42, b: "testing"}, "Text Here")

// and that becomes this element object:
{type: SomeComponent, props: {a: 42, b: "testing"}, children: ["Text Here"]}

renderの返り値であるJSX式はReact.createElementのシンタックスシュガーで、React.createElementの実行結果はReact Elements(UIの構造を現すプレーンなJSオブジェクト)になっています。

Reactはコンポーネントツリーから全体のrenderの結果(React Elements)を収集し、変更する必要がある全ての変更点のリストを作成します。 この際に行われる比較の差分と計算プロセスのことをReconciliationと呼んでいます。

つまりReact.createElementを実行した結果のオブジェクトのツリーを比較し、変更の必要があるコンポーネントを見つけることがレンダーフェーズと言えそうです。 後述のコミットフェーズでのDOMに適用する処理とは分けて考える必要があります。

A key part of this to understand is that “rendering” is not the same thing as “updating the DOM”, and a component may be rendered without any visible changes happening as a result.

翻訳:レンダリング」と「DOMの更新」は別物であり、コンポーネントは目に見える変化を伴わずにレンダリングされることがあるということを理解することが重要です。

コミットフェーズ

レンダーフェーズで収集した変更のリストから変更をDOMに適用するプロセスです。
レンダーフェーズとコミットフェーズが分離されていることで、変更の単位でDOMを更新することなく、一括でDOMに変更を反映できるわけですね。

コミットフェーズでDOM を更新した後、componentDidMountとcomponentDidUpdateのライフサイクルメソッドとuseLayoutEffectフックを同期的に実行します。 次に、Reactは短いタイムアウトを設定し、期限が切れるとすべてのuseEffectフックを実行します。このステップは「パッシブエフェクト」フェーズとも呼ばれるようです。

まとめ

  • 異なる型の要素の場合、Reactは要素以下のコンポーネントを全て破棄しゼロから再構築する
  • 同じ型の要素、DOMノードを保持したまま変更された属性のみ更新する
  • 同じ型のコンポーネントの場合、Reactはインスタンスを保持したままpropsを更新する
  • 子要素を再帰的に処理する場合、子要素リストの最初から比較いていき、差分を見つけたところから更新する
  • Reactのレンダリングプロセスにはレンダーフェーズとコミットフェーズがある
  • レンダーフェーズはReactElementsを収集し、差分検出(Reconciliation)をして変更する必要のあるコンポーネントを見つける
  • コミットフェーズはレンダーフェーズの結果からDOMに変更を適用し、その後useLayoutEffect, useEffectフックを実行する

この記事を書いた人

美髭公
美髭公事業開発部 web application engineer
2013年にアーティスに入社。システムエンジニアとしてアーティスCMSを使用したWebサイトや受託システムの構築・保守に携わる。環境構築が好き。
この記事のカテゴリ

FOLLOW US

最新の情報をお届けします