TL;DR
- React のレンダリングは Trigger → Render → Commit → Browser Paint の4フェーズに分けて理解できる
- Render フェーズは UI の計算だけで、DOM はまだ更新されない
- Commit フェーズで初めて DOM が更新され、ここは中断できない
- React 18 以降は Render フェーズが中断・破棄されうるため、副作用は Render に書いてはいけない
- Concurrent Rendering を前提に、
useTransition・useDeferredValue・Activity などの API が設計されている
はじめに
React がブラウザに UI を描画するまでに何をしているかについて、
「なんとなくこういう流れだよね」という理解はしていたものの、改めて整理してみると React の進化の方向性が見えてきました。
特に、
- React 17 までの「同期的なレンダリング」
- React 18 以降の「Concurrent Rendering」
- React 19 で追加された Activity などの仕組み
はすべて 同じ設計思想の延長線上にあることが理解できました。
この記事では、まずレンダリングの基本構造を押さえた上で、
その前提としてなぜ React が今の形に進化してきたのかを整理します。
4つのフェーズ
React が UI を描画する流れは、次の 4 フェーズに分けて考えると理解しやすくなります。
- Trigger(トリガー)
- Render(レンダー)
- Commit(コミット)
- Browser Paint(ブラウザペイント)
1. Trigger
Trigger は、その名の通り 「レンダリングを開始するきっかけ」 です。
React がレンダリングを開始する主なトリガーは次の 2 つです。
- 初期レンダリング
- 初期表示時に
createRoot().render()が呼ばれたとき
- 初期表示時に
- 再レンダリング
- 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のアップデートのインプット速度も上がる気がしました。