Concurrent Renderingとは
React 18で正式導入されたConcurrent Renderingは、Reactのレンダリングモデルにおける根本的なパラダイムシフトです。 従来のReactでは、一度レンダリングが開始されると完了するまで中断できませんでした。 Concurrent Renderingでは、レンダリングを中断・再開・破棄できるようになり、 同時に複数バージョンのUIツリーを準備することが可能になります。
この仕組みの基盤となっているのが、第3章で解説したFiberアーキテクチャです。 Fiber Nodeのリンクリスト構造により、Work Loopは任意の時点で作業を中断し、 より優先度の高いタスク(ユーザー入力など)を先に処理できます。
Time Slicing — レンダリングの分割実行
Time Slicingは、Concurrent Renderingの中核メカニズムです。 長いレンダリング処理を小さなチャンク(タイムスライス)に分割し、 各チャンクの間にブラウザにメインスレッドの制御を返します。
同期レンダリングとの違い
従来の同期レンダリングでは、コンポーネントツリー全体のレンダリングが完了するまでブラウザは他の処理を行えませんでした。 大量のリストやグラフを描画する場合、数百ミリ秒以上メインスレッドがブロックされ、ユーザー入力が無視されます。
sequenceDiagram
participant U as ユーザー入力
participant M as メインスレッド
participant S as 画面描画
Note over M: 【同期レンダリング】
M->>M: レンダリング開始 (200ms)
U--xM: クリック(無視される)
M->>S: DOM更新 & 描画
Note over U: 入力が遅延...
Note over M: 【Concurrent Rendering】
M->>M: チャンク1 (5ms)
U->>M: クリック(即座に処理)
M->>S: 緊急更新を描画
M->>M: チャンク2 (5ms)
M->>M: チャンク3 (5ms)
M->>S: 非緊急更新を描画Concurrent Renderingでは、各チャンクの実行後にブラウザのイベントキューを確認します。 ユーザー入力などの緊急イベントがあれば、現在のレンダリングを中断してそちらを優先的に処理します。 これにより、重い計算中でもUIが「固まらない」体験を実現できます。
Time Slicingの動作原理
ReactのWork Loopは、各Fiber Nodeの処理後に経過時間をチェックします。
デフォルトでは約5msのタイムスライスが設定されており、
この閾値を超えるとスケジューラがshouldYield()でtrueを返し、
ブラウザに制御を返します。残りの作業は次のフレームで継続されます。
// React内部のスケジューラの概念的な動作
function workLoop(deadline) {
let shouldYield = false;
while (nextUnitOfWork && !shouldYield) {
// 1つのFiber Nodeを処理
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
// 時間が残っているか確認(約5ms)
shouldYield = deadline.timeRemaining() < 1;
}
// まだ作業が残っていれば次のフレームに繰り越し
if (nextUnitOfWork) {
requestIdleCallback(workLoop);
}
} Suspense — 非同期UIの宣言的制御
<Suspense>は、コンポーネントが非同期データの準備完了を待っている間に
フォールバックUIを宣言的に表示する仕組みです。
React 16.6でlazy loadingのために導入され、React 18でデータフェッチへと適用範囲が拡大しました。
import { Suspense, lazy } from 'react';
const HeavyChart = lazy(() => import('./HeavyChart'));
function Dashboard() {
return (
<div>
<h1>ダッシュボード</h1>
<Suspense fallback={<p>チャートを読み込み中...</p>}>
<HeavyChart />
</Suspense>
</div>
);
} Suspenseをトリガーするもの
Suspenseのフォールバックを表示させるには、子コンポーネントが「まだ準備できていない」ことをReactに伝える必要があります。現在サポートされている方法は以下です:
React.lazy(): コンポーネントの遅延読み込みuse()Hook(React 19): Promise や Context の読み取り- Suspense対応フレームワーク: Next.js、Remix、Relay などのデータフェッチ
ネストによる段階的ローディング
Suspense境界はネストできます。これにより、ページの各セクションが独立したタイミングで表示されるようになり、 ユーザーは準備ができた部分から順に操作を開始できます。
function App() {
return (
<Suspense fallback={<PageSkeleton />}>
<Header />
<Suspense fallback={<SidebarSkeleton />}>
<Sidebar />
</Suspense>
<Suspense fallback={<ContentSkeleton />}>
<MainContent />
<Suspense fallback={<CommentsSkeleton />}>
<Comments />
</Suspense>
</Suspense>
</Suspense>
);
} 上記の例では、HeaderはページSkeleton解消後に即表示されます。 Sidebar、MainContent、Commentsはそれぞれ独立してロードされ、 準備ができた順にSkeletonから実コンテンツに置き換わります。
Transitions — 緊急・非緊急の分離
Transitionsは、UIの更新を「緊急」と「非緊急」に分類する仕組みです。 ユーザーが文字を入力する(緊急)操作と、その結果の検索結果を表示する(非緊急)操作は、 本質的に優先度が異なります。
useTransitionの使い方
import { useState, useTransition } from 'react';
function SearchPage() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
function handleChange(e) {
// 緊急: 入力フィールドは即座に更新
setQuery(e.target.value);
// 非緊急: 検索結果の更新はTransitionとしてマーク
startTransition(() => {
setResults(searchItems(e.target.value));
});
}
return (
<div>
<input value={query} onChange={handleChange} />
{isPending && <p>検索中...</p>}
<ResultList results={results} />
</div>
);
} startTransitionで囲まれた状態更新は「非緊急」としてスケジュールされます。
この間にユーザーが新たな入力を行えば、進行中のTransitionは中断され、
最新の入力に基づく新しいTransitionが開始されます。
isPendingフラグを使えば、Transitionが進行中であることをUIに反映できます。
| 特性 | 同期レンダリング | Concurrent Rendering |
|---|---|---|
| レンダリングの中断 | 不可(開始したら最後まで実行) | 可能(優先度に応じて中断・再開) |
| ユーザー入力への応答 | レンダリング完了まで待機 | 即座に応答(Time Slicing) |
| UI更新の優先度 | 全て同一優先度 | 緊急 / 非緊急に分離(Transitions) |
| 非同期データの表示 | 手動でローディング状態を管理 | 宣言的にフォールバック表示(Suspense) |
| 複数UIバージョンの準備 | 不可 | 可能(バックグラウンドで次のUIを構築) |
| 有効化方法 | デフォルト(React 17以前) | createRoot()で有効化 |
Selective Hydration — 段階的な復活
Selective Hydrationは、SSRとConcurrent Renderingの統合で実現された機能です。 従来のSSRでは、HTML全体の送信完了後にJavaScript全体の読み込みが必要で、 hydration(静的HTMLにイベントハンドラを結びつける処理)もページ全体に対して一括で行われていました。
ストリーミングSSRとの統合
React 18では、renderToPipeableStreamを使ったストリーミングSSRにより、
Suspense境界ごとにHTMLを段階的に配信できます。
各セクションのHTMLが生成され次第、ストリーミングでクライアントに送信されます。
// サーバーサイド
import { renderToPipeableStream } from 'react-dom/server';
const { pipe } = renderToPipeableStream(
<App />,
{
bootstrapScripts: ['/main.js'],
onShellReady() {
// シェル(Suspense外の部分)が準備完了
response.setHeader('content-type', 'text/html');
pipe(response);
},
}
); ユーザーインタラクション時の優先hydration
Selective Hydrationの最大の特徴は、ユーザーが操作しようとしたコンポーネントの hydrationを最優先で行うことです。 例えば、ページ上部のナビゲーションがまだhydration待ちの状態でユーザーがクリックすると、 Reactは他のセクションのhydrationを中断し、ナビゲーションのhydrationを先に実行します。
Offscreen Rendering(Activity)
Offscreen Renderingは長らく実験的機能でしたが、React 19.2で<Activity />コンポーネントとして正式に導入されました。
コンポーネントの状態を保持したまま表示・非表示を切り替えられる機能です。
Activityの基本的な使い方
import { Activity } from 'react';
function TabContainer({ activeTab }) {
return (
<div>
<Activity mode={activeTab === 'home' ? 'visible' : 'hidden'}>
<HomeTab />
</Activity>
<Activity mode={activeTab === 'profile' ? 'visible' : 'hidden'}>
<ProfileTab />
</Activity>
<Activity mode={activeTab === 'settings' ? 'visible' : 'hidden'}>
<SettingsTab />
</Activity>
</div>
);
} mode="hidden"にしたコンポーネントはDOMから隠されますが、React内部の状態(useState、useRef等)は保持されます。
再びmode="visible"に戻すと、状態がそのまま復元されます。
Activityの活用シーン
- タブ切り替え: 非アクティブなタブの状態を保持(入力中のフォーム、スクロール位置など)
- ページ遷移: 前のページを非表示状態で保持し、戻った時に即表示
- 仮想リストの先読み: 画面外のリストアイテムを低優先度でプリレンダリング
まとめ
Concurrent Renderingは、Reactがただの「UIライブラリ」から「ユーザー体験を最適化するランタイム」へと進化した象徴的な機能群です。 Time Slicingによるレスポンシブな操作性、Suspenseによる宣言的な非同期制御、Transitionsによる優先度管理、 Selective Hydrationによる段階的SSR、そしてActivityによる状態保持 — これらが組み合わさることで、 大規模アプリケーションでも「サクサク動く」UIを実現できます。
理解度チェック
Concurrent RenderingにおけるTime Slicingのデフォルトのタイムスライスは約何msですか?
キーボード: 1〜4 で選択、Enter で回答