TL;DR

  • React のレンダリングは Trigger → Render → Commit → Browser Paint の4フェーズに分けて理解できる
  • Render フェーズは UI の計算だけで、DOM はまだ更新されない
  • Commit フェーズで初めて DOM が更新され、ここは中断できない
  • React 18 以降は Render フェーズが中断・破棄されうるため、副作用は Render に書いてはいけない
  • Concurrent Rendering を前提に、useTransitionuseDeferredValue・Activity などの API が設計されている

はじめに

React がブラウザに UI を描画するまでに何をしているかについて、
「なんとなくこういう流れだよね」という理解はしていたものの、改めて整理してみると React の進化の方向性が見えてきました。

特に、

  • React 17 までの「同期的なレンダリング」
  • React 18 以降の「Concurrent Rendering」
  • React 19 で追加された Activity などの仕組み

はすべて 同じ設計思想の延長線上にあることが理解できました。

この記事では、まずレンダリングの基本構造を押さえた上で、
その前提としてなぜ React が今の形に進化してきたのかを整理します。


4つのフェーズ

React が UI を描画する流れは、次の 4 フェーズに分けて考えると理解しやすくなります。

  1. Trigger(トリガー)
  2. Render(レンダー)
  3. Commit(コミット)
  4. Browser Paint(ブラウザペイント)

1. Trigger

Trigger は、その名の通り 「レンダリングを開始するきっかけ」 です。
React がレンダリングを開始する主なトリガーは次の 2 つです。

  1. 初期レンダリング
    • 初期表示時に createRoot().render() が呼ばれたとき
  2. 再レンダリング
    • stateが更新された時
    • 親コンポーネントの再レンダリング 等

Trigger が発生すると、次の Render フェーズ が始まります。


2. Render

Render フェーズでは 「UI の計算」 を行います。

「レンダー」という言葉から画面に描画されているように聞こえますが、
このフェーズでは DOM は一切更新されません

Render フェーズで行われることは次の通りです。

  • 関数コンポーネントの実行
  • JSX をもとに Fiber Tree(次の UI 候補)の構築
  • 前回の Fiber Tree との比較(Reconciliation)
  • どの DOM を変更する必要があるかの判定

重要なのは、Render フェーズは 純粋な計算フェーズ だという点です。

Render フェーズの重要な性質

React 18 以降では、Render フェーズは次の性質を持ちます。

  • 中断されることがある
  • 途中まで計算しても破棄されることがある
  • 同じコンポーネントが複数回実行されることがある

そのため Render フェーズでは、

「何回実行されても問題ない処理」

しか書いてはいけません。

この性質があるため、Render フェーズに副作用を書いてはいけないというルールが生まれています。


3. Commit

Render フェーズが最後まで完了すると、次に Commit フェーズ に進みます。

Commit フェーズは、

  • 同期的
  • 中断不可
  • 必ず最後まで実行される

という特徴を持っています。

ここで初めて 実際の DOM が更新されます。

Commit フェーズは、内部的に次の 3 ステップに分かれています。


3-1. Before Mutation

  • タイミング:DOM が書き換わる直前

このフェーズでは、DOM が変更される「前の状態」を使った処理が行われます。

用途例

  • スクロール位置の保存
  • DOM 変更前の状態取得

3-2. Mutation(DOM 更新)

ここで 初めて DOM に変更が加えられます

Render フェーズで計算された差分をもとに、実際の DOM 操作が行われます。

実行される操作例:

  • appendChild(要素の追加)
  • removeChild(要素の削除)
  • 属性の更新

3-3. Layout Effects

  • タイミング:DOM 更新直後、ブラウザが描画する前
  • 呼ばれるもの
    • useLayoutEffect

このフェーズでは、DOM の状態が確定しているため、

  • 要素サイズの測定
  • レイアウトに依存した処理
  • 描画前に必要な同期的調整

などが可能です。

useLayoutEffect が重いと、ブラウザの描画をブロックするため、使い所は慎重に選ぶ必要があります。


4. Browser Paint

Commit フェーズが完了すると、React の仕事は一旦終わります。
その後は ブラウザ自身が次の処理を行います。

  • スタイル計算
  • レイアウト計算(Reflow)
  • ピクセル描画(Repaint)

このフェーズで 初めてユーザーの画面に UI が表示されます。

また、useEffect はこの Browser Paint の後 に非同期的に実行されます。

これにより、

  • 画面表示をブロックしない
  • ユーザー操作を優先する

という React の設計意図が実現されています。


Concurrent Rendering

React 18 から導入された Concurrent Rendering は、「レンダリングをスケジューリングできる」 仕組みです。

React 17 まで

  • Render は基本的に最後まで一気に実行
  • 重い Render があると UI が固まりやすい

React 18 以降

  • Render フェーズは中断・再開・破棄が可能
  • ユーザー入力など 優先度の高い更新を先に処理できる

この仕組みにより、

  • Render フェーズは「仮の計算」
  • Commit フェーズだけが「現実を変える」

という役割分担がより明確になっています。


Concurrent Rendering を前提とした React v18 / v19 の機能

Concurrent Rendering を前提として、React 18 / 19 では次のような API が提供されています。

useTransition / startTransition

useTransition / startTransition は、更新の優先度を下げるためのAPIです。

  • 「少し遅れてもよい更新」を明示的に表現できる

useDeferredValue

useDeferredValue は、値の反映を遅らせるためのhooksです。

  • 重い計算や描画に使う値は遅延させることができる

useTransition が「更新そのものの優先度」を下げるのに対し、 useDeferredValue は「表示に使う値」を遅らせる点が違いです。

Activity(React 19)

Activity は、

  • コンポーネントをアンマウントせず
  • state や DOM を保持したまま
  • 更新だけを止める

ための仕組みです。

親で state を管理したり、display: none を使うだけでは難しい、

  • エディタのカーソル位置
  • スクロール状態
  • 内部 state を持つ重い UI

といったケースで、限定的に効果を発揮します。


おわりに

Reactのレンダリング流れを調べる中で、Reactの設計思想についての理解も深まりました。 これを理解しているだけで、今後のReactのアップデートのインプット速度も上がる気がしました。