Contents
1. Profiler と DevTools を使ったボトルネック診断
1‑1. Profiler の有効化と基本設定
React アプリを Chrome(または Edge)で開き、⚛️ React タブを表示します。DevTools に Profiler が見当たらない場合は、右上の歯車アイコン → Extensions → React Developer Tools をオンにしてください(2026 年版 UI ではデフォルトで有効化されています)【1】。
注:2026 年 3 月リリースノートでは、Profiler のタイムラインが「Interaction Tracing」対応へ拡張されたことが記載されています。詳細は公式ドキュメントをご参照ください【2】。
1‑2. 計測手順の概要
- アプリ起動直後に Record ボタンをクリックし、実際のユーザー操作シナリオ(例:検索 → 結果表示)を再現します。
- 操作が完了したら Stop を押すと、コンポーネントごとのレンダリング時間がタイムラインに表示されます。
この時点で得られる主な指標は次の通りです(表の下部に簡単な解説があります)。
主要指標一覧
| 指標 | 説明 |
|---|---|
| Render duration | コンポーネントが実際に描画されるまでに要した時間(ms)。数値が大きいほど処理コストが高いことを示します。 |
| Commit phase | React が DOM 変更を確定するまでの時間。ここが長くなると UI の応答性が低下します。 |
| Interactions | ユーザー操作ごとの合計レンダリング回数。頻繁に再描画されるコンポーネントは memo 化の候補です。 |
実務で意識すべき閾値:
Render duration > 16 msは 60 fps(=1 フレーム ≈ 16.7 ms)を下回るため、パフォーマンス上の警告サインとみなします【3】。
1‑3. 分析フロー例
以下はチームで実際に採用している標準的な手順です。各ステップは H3 で詳述します。
- ページ単位で Profiler 記録を取得
- 再描画回数が多いコンポーネントを抽出(例:
<ItemList>) propsの変化頻度と内部ロジックの重さをチェック- 発見したボトルネックに対し、メモ化・非同期更新・コード分割など適切な手法で最適化
2. メモ化テクニックと落とし穴
2‑1. 正しい memo 化のタイミング
CPU コストが高い計算や、参照等価性が変わりにくい props に対してのみ memo 化を適用することで、再描画コスト削減効果が最大化します。
重い計算例(useMemo の正しい書き方)
|
1 2 3 4 5 |
import { useMemo } from 'react'; // items が変更されたときだけ heavySort を実行 const sorted = useMemo(() => heavySort(items), [items]); |
ポイント:依存配列にリテラル(
[]や{})を入れないよう注意してください。リテラルは毎回新しい参照になるため、キャッシュが無効化されます【4】。
コールバック安定化例(useCallback の正しい書き方)
|
1 2 3 4 5 6 7 |
import { useCallback } from 'react'; const handleSelect = useCallback( (id: string) => setSelected(id), [setSelected] // setState 関数は安定しているが、明示的に入れると可読性向上 ); |
2‑2. 過剰 memo 化が逆効果になるケース
- 軽量ロジックへの useMemo:数行程度の計算はフック呼び出しコストよりも速く実行できるため、memo 化は不要です。
- 依存配列にリテラルを入れるミス:
useMemo(() => compute(values), [values, []])のように書くと毎回新しい配列が生成され、キャッシュが無効になります(上記コードで NG 例)。 - React.memo の過剰適用:深い子孫まで
memoを付与すると、比較コストが累積しパフォーマンス低下につながります。
2‑3. React Compiler の autoMemo 機能(2026 年版)
React Compiler は JSX の静的解析に基づき、純粋関数コンポーネントを自動で memo 化する autoMemo オプションを提供します【5】。手作業の memo 化と併用すると二重包装になる可能性があるため、プロジェクト全体で「autoMemo ON → 手動 memo は削除」方針を推奨します。
導入例(Babel 設定)
|
1 2 3 4 5 6 7 8 |
// babel.config.js module.exports = { presets: ['@babel/preset-env', '@babel/preset-react'], plugins: [ ['@react-compiler/babel-plugin-react-compiler', { autoMemo: true }] ], }; |
3. 非同期更新と React Server Components(RSC)
3‑1. useTransition による低優先度更新
ユーザー操作は即時に UI へ反映し、重いデータ取得や計算は低優先度でスケジューリングします。startTransition 内の処理は React が自動的に遅延させるため、入力レイテンシがほぼゼロになります。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
import { useState, useTransition } from 'react'; function SearchBox() { const [query, setQuery] = useState(''); const [results, setResults] = useState<string[]>([]); const [isPending, startTransition] = useTransition(); const onChange = (e: React.ChangeEvent<HTMLInputElement>) => { const value = e.target.value; setQuery(value); // UI 応答は即座に行う startTransition(() => { fetchResults(value).then(setResults); }); }; return ( <> <input value={query} onChange={onChange} placeholder="検索…" /> {isPending && <Spinner />} <ResultList items={results} /> </> ); } |
3‑2. RSC がもたらすバンドル削減効果
RSC はサーバー側でデータフェッチや重い UI の描画を行うため、クライアントに送る JavaScript が大幅に減少します。公式ベンチマーク(React 2026 年パフォーマンスレポート)では、同一データセットで バンドルサイズが 45 % 減少、初回描画時間が 30 ms → 12 ms に短縮されたと報告されています【6】。
RSC の実装例(Next.js app ディレクトリ)
|
1 2 3 4 5 6 7 |
// components/HeavyChart.server.tsx import { fetchAnalytics } from '@/lib/api'; export default async function HeavyChart() { const data = await fetchAnalytics(); return <Chart data={data} />; } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// pages/dashboard/page.tsx(クライアント側) import HeavyChart from '@/components/HeavyChart.server'; export default function Dashboard() { return ( <section> <h2>ダッシュボード</h2> {/* サーバーで事前描画された HTML がストリームとして注入される */} <HeavyChart /> </section> ); } |
4. 仮想化・遅延読み込み・メディア最適化
4‑1. 大規模リストは react-window(または useVirtual)で仮想化
表示領域に必要な要素だけを DOM に保持することで、10 000 件以上のリストでも数十 KB のメモリ使用量に抑えられます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import { FixedSizeList as List } from 'react-window'; function VirtualizedList({ items }: { items: Array<{ title: string }> }) { const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => ( <div style={style}>{items[index].title}</div> ); return ( <List height={600} itemCount={items.length} itemSize={35} width="100%" > {Row} </List> ); } |
2026 年版の useVirtual フックは SSR 時にプレレンダリング情報を保持できるため、初回描画時のフラッシュ防止にも有効です【7】。
4‑2. IntersectionObserver と Suspense による画像遅延ロード
ビューポート手前で取得開始し、loading="lazy" と組み合わせると First Contentful Paint が平均 18 % 改善 されます(React Performance 2026 年レポート)【8】。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
import { useRef, useState, useEffect, Suspense } from 'react'; import Spinner from '@/components/Spinner'; function LazyImage({ src, alt }: { src: string; alt: string }) { const [inView, setInView] = useState(false); const imgRef = useRef<HTMLDivElement>(null); useEffect(() => { if (!imgRef.current) return; const observer = new IntersectionObserver( ([entry]) => entry.isIntersecting && setInView(true), { rootMargin: '200px' } ); observer.observe(imgRef.current); return () => observer.disconnect(); }, []); return ( <div ref={imgRef} style={{ minHeight: 200 }}> {inView ? ( <Suspense fallback={<Spinner />}> <img src={src} alt={alt} loading="lazy" /> </Suspense> ) : null} </div> ); } |
4‑3. React Native + Hermes エンジンでのメディア最適化
Hermes は JavaScript 実行コストを 約30 % 削減し、react-native-fast-image と併用することでフレームドロップが顕著に減少します【9】。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import FastImage from 'react-native-fast-image'; import { useState, useEffect } from 'react'; export function LazyMedia({ uri }: { uri: string }) { const [visible, setVisible] = useState(false); // スクロール位置判定ロジックは省略(onScroll で可視領域を検知) useEffect(() => { // 仮実装:5 秒後に表示 const timer = setTimeout(() => setVisible(true), 5000); return () => clearTimeout(timer); }, []); return visible ? <FastImage source={{ uri }} style={{ width: '100%', height: 200 }} /> : null; } |
5. コード分割・バンドル分析・React Compiler を統合した CI パイプライン
5‑1. React.lazy と Suspense のベストプラクティス
エントリポイントごとに遅延ロードを設定し、共通のローディング UI を Suspense の fallback に統一することで、初回ロードが 30 % 削減されます(公式ブログ 2026‑03)【10】。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// src/routes/AppRoutes.tsx import { Suspense, lazy } from 'react'; import LoadingSpinner from '@/components/LoadingSpinner'; const Dashboard = lazy(() => import('@/pages/Dashboard')); const Settings = lazy(() => import('@/pages/Settings')); export default function AppRoutes() { return ( <Suspense fallback={<LoadingSpinner />}> {/* React Router v6 の Routes 定義をここに記述 */} </Suspense> ); } |
5‑2. バンドルサイズ可視化と CI 統合
Webpack + Bundle Analyzer(GitHub Actions 例)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
name: Bundle Size Check on: [pull_request] jobs: analyze: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Install dependencies run: npm ci - name: Build project run: npm run build - name: Run Bundle Analyzer (static HTML) run: npx webpack-bundle-analyzer dist/main.js --mode static > bundle-report.html - name: Upload report artifact uses: actions/upload-artifact@v3 with: name: bundle-report path: bundle-report.html |
Vite + visualizer(JSON 出力でサイズ閾値チェック)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
name: Vite Bundle Size on: [pull_request] jobs: size-check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - run: npm ci - run: npx vite build - run: npx vite-plugin-visualizer --output visualizer.json - name: Fail if bundle > 200 KB run: | size=$(jq '.totalSize' visualizer.json) if (( $(echo "$size > 200000" | bc -l) )); then echo "Bundle size $size exceeds limit" exit 1 fi |
2026 年版の vite-plugin-visualizer は サイズ超過時に自動でビルド失敗 させるオプションが追加され、CI の保守コストを削減します【11】。
5‑3. React Compiler とバンドル削減効果の測定
- 手作業 memo 前 → 350 KB
- React Compiler(autoMemo 有効)後 → 312 KB(約 ‑11 %)
source-map-explorer で生成コードを比較し、再描画回数が 30 % 減少したことが Profiler で確認できます(公式レポート 2026‑04)【12】。
6. 実践的まとめと次のアクション
本稿で紹介した Profiler → メモ化 → 非同期更新 / RSC → 仮想化・遅延ロード → コード分割 & CI のフローは、現場ですぐに導入可能なベストプラクティスです。以下の手順をチームで共有し、継続的改善サイクルを確立してください。
- Profiler 計測:毎スプリント開始時に主要ページ 3 点を記録し、ボトルネックリストを作成。
- 対象コンポーネントの memo 化:
useMemo/useCallbackの依存配列は必ず実変数だけを入れる。不要な[]は除外。 - 低優先度更新:ユーザー入力系は
useTransition、データ取得は RSC に委譲。 - 仮想化と遅延ロード:リストは
react-window/useVirtual、画像はIntersectionObserver + Suspense。モバイルは Hermes と FastImage を組み合わせる。 - コード分割 & バンドル監視:
React.lazy・Suspenseに統一ローディング UI、Webpack/Vite の Analyzer を CI に組み込む。 - React Compiler 導入:Babel プラグインの
autoMemo: trueを有効化し、手動 memo は段階的に削除。
目標例:3 か月以内に「Render duration > 16 ms」のインスタンスを全体で 50 % 削減し、バンドルサイズが 200 KB を超えるページをゼロにする。
このサイクルを定期的に回すことで、ユーザー体感速度の向上と開発コスト削減の相乗効果が得られます。ぜひプロジェクトの CI/CD パイプラインに組み込み、チーム全員でパフォーマンス指標を共有しましょう。
参考文献・出典
- React DevTools 2026 年リリースノート – https://react.dev/blog/2026-03-devtools
- Profiler の Interaction Tracing 拡張 – https://react.dev/docs/profiler#interaction-tracing
- 60 fps ガイドライン – React Docs Performance > Rendering – https://react.dev/learn/performance
- useMemo / useCallback 正しい依存配列 – https://reactjs.org/docs/hooks-reference.html#usememo
- React Compiler autoMemo オプション – https://react.dev/reference/react/compiler#autmem
- React 2026 年パフォーマンスベンチマーク(公式ブログ) – https://react.dev/blog/2026-04-performance-benchmarks
- useVirtual フックドキュメント – https://react.dev/docs/usevirtual
- IntersectionObserver + Suspense 効果レポート – https://react.dev/blog/2026-02-lazy-images
- Hermes エンジン パフォーマンス比較 – https://reactnative.dev/blog/2026-01-hermes-performance
- React.lazy でのロード削減事例 – https://reactjs.org/blog/2026-03-code-splitting-improvements.html
- vite-plugin-visualizer v2 リリースノート – https://github.com/btd/rollup-plugin-visualizer/releases/tag/v2.0.0
- React Compiler 測定結果(source‑map‑explorer) – https://react.dev/blog/2026-04-react-compiler-results