mcpField
R
react-performance

react-performance

github.com/claude-dev-suite開発4643 回インストール4 日前に更新
リポジトリ

react-performance について

Reactアプリを最適化する開発者向けのパフォーマンススキルです。プロファイリング、メモ化、仮想化、コード分割、状態更新の手法と、よくある問題の調査方法を扱います。

主な機能

  1. 01React APIによるメモ化
  2. 02長いリストの仮想化
  3. 03ルートとコンポーネントのコード分割
  4. 04状態更新の最適化
  5. 05パフォーマンス分析と問題調査

活用シーン

  1. 01コンポーネントの不要な再レンダーの削減
  2. 02大きなリストのスクロール改善
  3. 03コード分割による初期読み込みの短縮
  4. 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 のペイロードが大きいツリーシェイキングされていない必要なものだけをインポートする

リファレンスドキュメント

コメント