react-performance について
Reactアプリを最適化する開発者向けのパフォーマンススキルです。プロファイリング、メモ化、仮想化、コード分割、状態更新の手法と、よくある問題の調査方法を扱います。
主な機能
- 01React APIによるメモ化
- 02長いリストの仮想化
- 03ルートとコンポーネントのコード分割
- 04状態更新の最適化
- 05パフォーマンス分析と問題調査
活用シーン
- 01コンポーネントの不要な再レンダーの削減
- 02大きなリストのスクロール改善
- 03コード分割による初期読み込みの短縮
- 04更新の遅さやバンドル肥大化の診断
MCP 対応
Reactのパフォーマンス文書の参照先として、`mcp__documentation__fetch_docs`を記載しています。
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ claude-dev-suite/ claude-dev-suite --skill react-performanceインストール後、Claude Code で/react-performanceを入力して読み込まれたか確認します。
同梱ファイルadvanced.md
詳細
React のパフォーマンス
深い知識:React のパフォーマンス最適化とプロファイリング手法に関する包括的なドキュメントは、technology:
react、topic:performanceを指定してmcp__documentation__fetch_docsを使用してください。
完全なリファレンス:React DevTools によるプロファイリング、バンドルの最適化、画像の最適化、Web Workers については、advanced.md を参照してください。
メモ化
React.memo
props に変更がない場合の再レンダリングを防ぎます:
// Only re-renders when props change (shallow comparison)
const ExpensiveComponent = memo(function ExpensiveComponent({
data,
onItemClick,
}: Props) {
return (
<ul>
{data.map(item => (
<li key={item.id} onClick={() => onItemClick(item.id)}>
{item.name}
</li>
))}
</ul>
);
});
// With custom comparison
const OptimizedComponent = memo(
function OptimizedComponent({ user }: Props) {
return <div>{user.name}</div>;
},
(prevProps, nextProps) => prevProps.user.id === nextProps.user.id
);
memo を使うタイミング
// ✅ Good: Expensive component with stable parent
const ExpensiveList = memo(function ExpensiveList({ items }: { items: Item[] }) {
return items.map(item => <ExpensiveItem key={item.id} item={item} />);
});
// ❌ Bad: Simple component, memo overhead not worth it
const SimpleText = memo(function SimpleText({ text }: { text: string }) {
return <span>{text}</span>;
});
// ❌ Bad: Props always change anyway
function Parent() {
// New object on every render - memo is useless!
return <MemoizedChild data={{ value: 1 }} />;
}
useMemo
負荷の高い計算をメモ化します:
function ProductList({ products, filter }: Props) {
const filteredProducts = useMemo(() => {
return products
.filter(p => p.category === filter.category)
.filter(p => p.price >= filter.minPrice && p.price <= filter.maxPrice)
.sort((a, b) => a.name.localeCompare(b.name));
}, [products, filter.category, filter.minPrice, filter.maxPrice]);
return (
<ul>
{filteredProducts.map(p => <ProductCard key={p.id} product={p} />)}
</ul>
);
}
// ❌ Don't overuse - simple operations don't need memoization
const total = useMemo(() => a + b, [a, b]); // Overkill!
useCallback
子コンポーネントの再レンダリングを防ぐため、関数をメモ化します:
function Parent() {
const [count, setCount] = useState(0);
// ✅ Stable reference for child components
const handleClick = useCallback((id: string) => {
console.log('Clicked:', id);
}, []);
// ✅ Dependencies that change function behavior
const handleSubmit = useCallback((data: FormData) => {
submitWithCount(data, count);
}, [count]);
return (
<>
<ChildComponent onClick={handleClick} />
<Form onSubmit={handleSubmit} />
</>
);
}
仮想化
大きなリストでは、表示されている項目のみをレンダリングします:
import { FixedSizeList } from 'react-window';
function VirtualList({ items }: { items: Item[] }) {
const Row = ({ index, style }: { index: number; style: CSSProperties }) => (
<div style={style} className="list-item">
{items[index].name}
</div>
);
return (
<FixedSizeList
height={400}
width="100%"
itemCount={items.length}
itemSize={50}
>
{Row}
</FixedSizeList>
);
}
TanStack Virtual を使う場合
import { useVirtualizer } from '@tanstack/react-virtual';
function VirtualList({ items }: { items: Item[] }) {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
overscan: 5,
});
return (
<div ref={parentRef} style={{ height: 400, overflow: 'auto' }}>
<div style={{ height: virtualizer.getTotalSize(), position: 'relative' }}>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: virtualItem.size,
transform: `translateY(${virtualItem.start}px)`,
}}
>
{items[virtualItem.index].name}
</div>
))}
</div>
</div>
);
}
コード分割
ルート単位の分割
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';
const Home = lazy(() => import('./pages/Home'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
function App() {
return (
<Suspense fallback={<PageLoader />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
コンポーネント単位の分割
const HeavyChart = lazy(() => import('./HeavyChart'));
function Dashboard() {
const [showChart, setShowChart] = useState(false);
return (
<div>
<button onClick={() => setShowChart(true)}>Show Chart</button>
{showChart && (
<Suspense fallback={<ChartSkeleton />}>
<HeavyChart data={data} />
</Suspense>
)}
</div>
);
}
プリロード
const Dashboard = lazy(() => import('./Dashboard'));
const preloadDashboard = () => import('./Dashboard');
function NavLink() {
return (
<Link
to="/dashboard"
onMouseEnter={preloadDashboard}
onFocus={preloadDashboard}
>
Dashboard
</Link>
);
}
不要な再レンダリングの回避
安定した参照
// ❌ Bad: New object on every render
function Parent() {
return <Child style={{ color: 'red' }} />;
}
// ✅ Good: Stable reference
const style = { color: 'red' };
function Parent() {
return <Child style={style} />;
}
// ✅ Good: useMemo for dynamic values
function Parent({ color }) {
const style = useMemo(() => ({ color }), [color]);
return <Child style={style} />;
}
コンポーネントの合成
// ❌ Bad: Entire component re-renders on count change
function App() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
<ExpensiveComponent /> {/* Re-renders on every count change! */}
</div>
);
}
// ✅ Good: Move state down
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
function App() {
return (
<div>
<Counter />
<ExpensiveComponent /> {/* Doesn't re-render! */}
</div>
);
}
// ✅ Good: Pass children as props
function Counter({ children }) {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
{children} {/* Doesn't re-render! */}
</div>
);
}
状態管理の最適化
更新のバッチ処理
// React 18+ automatically batches these
function handleClick() {
setCount(c => c + 1);
setFlag(f => !f);
setText('updated');
// Only ONE re-render!
}
派生状態
// ❌ Bad: Synchronized state
function Form() {
const [items, setItems] = useState([]);
const [total, setTotal] = useState(0);
useEffect(() => {
setTotal(items.reduce((sum, item) => sum + item.price, 0));
}, [items]); // Extra re-render!
}
// ✅ Good: Calculate during render
function Form() {
const [items, setItems] = useState([]);
const total = items.reduce((sum, item) => sum + item.price, 0);
}
// ✅ Good: useMemo for expensive calculations
function Form() {
const [items, setItems] = useState([]);
const total = useMemo(
() => items.reduce((sum, item) => sum + item.price, 0),
[items]
);
}
よくある落とし穴
| 問題 | 原因 | 解決策 |
|---|---|---|
| 初回レンダリングが遅い | バンドルが大きい | コード分割、遅延読み込み |
| 更新が遅い | 再レンダリングが多すぎる | memo、useMemo、useCallback |
| スクロールがぎこちない | リストの項目をすべてレンダリングしている | 仮想化 |
| メモリリーク | クリーンアップされていないエフェクト | 適切なクリーンアップ関数 |
| レイアウトのスラッシング | 同期レイアウトの強制 | DOM の読み取り/書き込みをバッチ処理 |
ベストプラクティス
- ✅ 最適化の前にプロファイルする
- ✅ React DevTools Profiler を使う
- ✅ 長いリスト(100 項目超)を仮想化する
- ✅ ルート単位でコードを分割する
- ✅ 負荷の高い計算をメモ化する
- ✅ オブジェクト/関数には安定した参照を使う
- ❌ 時期尚早に最適化しすぎない
- ❌ すべてを memo でラップしない
- ❌ 重い JavaScript でメインスレッドをブロックしない
このスキルを使わない場合
- React 19 コンパイラーの最適化 - コンパイラー固有の機能には
react-19スキルを使ってください - 基本的な React 開発 - 一般的なコンポーネント開発には
reactスキルを使ってください - テスト - パフォーマンステストの戦略には
react-testingスキルを使ってください - パフォーマンス以外の React の問題 - このスキルは最適化に特化しています
アンチパターン
| アンチパターン | 問題 | 解決策 |
|---|---|---|
| 時期尚早な最適化 | 労力の浪費、コードの複雑化 | まずプロファイルし、重要な箇所を最適化する |
| すべてを memo でラップする | オーバーヘッドが発生し、効果がない | 負荷の高いコンポーネントのみをメモ化する |
| 負荷の軽い計算に useMemo を使う | 得られる効果よりオーバーヘッドが大きい | 負荷の高い処理のみをメモ化する |
| JSX 内のインラインオブジェクト/関数 | 子コンポーネントのメモ化が機能しなくなる | 定数に切り出すか、useCallback を使う |
| key prop を正しく使わない | リスト全体が再レンダリングされる | 安定した一意の ID を使う |
| リスト全体をレンダリングする | スクロールが遅くなる | 100 項目超では仮想化を使う |
| コード分割なしの大きなバンドル | 初回読み込みが遅くなる | ルート単位で分割する |
すばやいトラブルシューティング
| 問題 | 考えられる原因 | 対処法 |
|---|---|---|
| 初回読み込みが遅い | バンドルサイズが大きい | コードを分割し、ルートを遅延読み込みする |
| スクロールがぎこちない | リストの項目をすべてレンダリングしている | react-window または TanStack Virtual を使う |
| 不要な再レンダリングが頻繁に発生する | props の参照同一性が変化している | useMemo/useCallback でオブジェクト/関数をメモ化する |
| コンポーネントの更新が遅い | レンダリング中の計算負荷が高い | useMemo または Web Worker に移す |
| メモリリーク | クリーンアップされていないエフェクト/サブスクリプション | useEffect にクリーンアップ関数を追加する |
| JavaScript のペイロードが大きい | ツリーシェイキングされていない | 必要なものだけをインポートする |
リファレンスドキュメント
- React Profiler
- パフォーマンスの最適化
- MCP:
mcp__documentation__fetch_docs→ technology:react、topic:performance
コメント
Markdown 対応