Reactアプリが遅くなる原因と最適化の考え方
Reactアプリケーションが大きくなるにつれて、パフォーマンスの問題が顕在化してきます。主な原因は不要な再レンダリングと重い計算処理です。この記事では、Reactのパフォーマンス最適化テクニックを体系的に解説します。
Reactのレンダリングの仕組み
Reactは以下のいずれかの条件で再レンダリングが発生します。
- コンポーネントのstateが変化した時
- コンポーネントのpropsが変化した時
- 親コンポーネントが再レンダリングされた時
- useContextのcontextが変化した時
React.memoでコンポーネントのメモ化
import React, { memo } from 'react';
// メモ化なし:親が再レンダリングされるたびに再レンダリング
function ExpensiveComponent({ data, onUpdate }) {
console.log('レンダリング実行');
return <div>{data.name}</div>;
}
// React.memoでメモ化:propsが変わらない限り再レンダリングしない
const MemoizedComponent = memo(function ExpensiveComponent({ data, onUpdate }) {
console.log('レンダリング実行');
return <div>{data.name}</div>;
});
// カスタム比較関数(深い比較が必要な場合)
const MemoWithCustomCompare = memo(
ExpensiveComponent,
(prevProps, nextProps) => {
return prevProps.data.id === nextProps.data.id &&
prevProps.data.name === nextProps.data.name;
}
);
useMemoで計算結果をメモ化
import { useMemo, useState } from 'react';
function ProductList({ products, filterText, sortBy }) {
// filterTextかsortByが変わった時だけ再計算
const filteredAndSortedProducts = useMemo(() => {
console.log('フィルタリング・ソート処理実行');
return products
.filter(p => p.name.toLowerCase().includes(filterText.toLowerCase()))
.sort((a, b) => {
if (sortBy === 'price') return a.price - b.price;
if (sortBy === 'name') return a.name.localeCompare(b.name);
return 0;
});
}, [products, filterText, sortBy]);
return (
<ul>
{filteredAndSortedProducts.map(p => (
<li key={p.id}>{p.name}: ¥{p.price.toLocaleString()}</li>
))}
</ul>
);
}
useCallbackで関数をメモ化
import { useCallback, useState } from 'react';
function ParentComponent() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
// countが変わってもhandleClickは同じ関数オブジェクトを返す
// → MemoizedChildは不要な再レンダリングをしない
const handleClick = useCallback((id) => {
console.log('クリック:', id);
// 最新のstateが必要な場合は関数形式で更新
setCount(prev => prev + 1);
}, []); // 依存関係なし
return (
<div>
<p>Count: {count}</p>
<input value={text} onChange={e => setText(e.target.value)} />
<MemoizedChild onClick={handleClick} />
</div>
);
}
Code Splitting と遅延読み込み
import React, { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';
// 重いコンポーネントを遅延読み込み
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Analytics = lazy(() => import('./pages/Analytics'));
const Settings = lazy(() => import('./pages/Settings'));
function App() {
return (
<Suspense fallback={<div className="loading">読み込み中...</div>}>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/analytics" element={<Analytics />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
仮想スクロールで大量データを処理
import { FixedSizeList as List } from 'react-window';
// 10万件のデータでも軽快に表示
function VirtualizedList({ items }) {
const Row = ({ index, style }) => (
<div style={style}>
{items[index].name}: {items[index].value}
</div>
);
return (
<List
height={500} // 表示エリアの高さ
itemCount={items.length}
itemSize={35} // 各行の高さ
width="100%"
>
{Row}
</List>
);
}
React DevToolsでパフォーマンスを計測する
React DevToolsのProfilerタブを使って、どのコンポーネントが遅いかを特定できます。
- ChromeにReact Developer Toolsをインストール
- DevToolsを開き「Profiler」タブを選択
- 「録画開始」ボタンを押して操作を行う
- 「録画停止」で結果を分析
- 「Flamegraph」で各コンポーネントのレンダリング時間を確認
パフォーマンス最適化のベストプラクティス
| テクニック | 使いどころ | 効果 |
|---|---|---|
| React.memo | propsが変わりにくいコンポーネント | 不要な再レンダリングを防止 |
| useMemo | 重い計算処理 | 計算結果をキャッシュ |
| useCallback | 子コンポーネントに渡す関数 | 関数オブジェクトの再生成を防止 |
| lazy/Suspense | 大きなコンポーネント | 初期読み込み時間を短縮 |
| 仮想スクロール | 長大なリスト | DOM要素数を削減 |
まとめ
- ✅ まずはDevToolsで問題箇所を特定する
- ✅ React.memo・useMemo・useCallbackを適切に使用
- ✅ Code Splittingで初期ロード時間を短縮
- ✅ 大量データにはreact-windowの仮想スクロールを活用
過剰な最適化は逆効果になることもあります。まずProfilerで計測し、本当にボトルネックになっている箇所だけを最適化しましょう。