Back to Blog

React パフォーマンス最適化の完全ガイド

Minh Duy
Published on January 2, 2025
7 min read
React パフォーマンス最適化の完全ガイド
Frontend
React
パフォーマンス
最適化
JavaScript

React パフォーマンス最適化の完全ガイド

Reactアプリケーションのパフォーマンス最適化は、ユーザーエクスペリエンスを向上させるために重要です。この記事では、実践的な最適化手法を詳しく解説します。

パフォーマンス測定

React DevTools Profiler

// プロファイリングの開始
import { Profiler } from 'react';

function onRenderCallback(id, phase, actualDuration, baseDuration, startTime, commitTime) {
  console.log('Component:', id);
  console.log('Phase:', phase);
  console.log('Actual duration:', actualDuration);
  console.log('Base duration:', baseDuration);
}

function App() {
  return (
    <Profiler id="App" onRender={onRenderCallback}>
      <Header />
      <Main />
      <Footer />
    </Profiler>
  );
}

Performance API

// カスタムパフォーマンス測定
function measureComponentRender(componentName, renderFunction) {
  performance.mark(`${componentName}-start`);
  const result = renderFunction();
  performance.mark(`${componentName}-end`);
  performance.measure(
    `${componentName}-render`,
    `${componentName}-start`,
    `${componentName}-end`
  );
  
  const measure = performance.getEntriesByName(`${componentName}-render`)[0];
  console.log(`${componentName} render time: ${measure.duration}ms`);
  
  return result;
}

メモ化による最適化

React.memo

// 不要な再レンダリングを防ぐ
const ExpensiveComponent = React.memo(({ data, onUpdate }) => {
  console.log('ExpensiveComponent rendered');
  
  return (
    <div>
      {data.map(item => (
        <div key={item.id}>
          <h3>{item.title}</h3>
          <p>{item.description}</p>
          <button onClick={() => onUpdate(item.id)}>
            更新
          </button>
        </div>
      ))}
    </div>
  );
});

// カスタム比較関数
const OptimizedComponent = React.memo(({ user, settings }) => {
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{settings.theme}</p>
    </div>
  );
}, (prevProps, nextProps) => {
  // userのnameとsettingsのthemeが変わらない場合は再レンダリングしない
  return prevProps.user.name === nextProps.user.name &&
         prevProps.settings.theme === nextProps.settings.theme;
});

useMemo

import { useMemo, useState } from 'react';

function DataVisualization({ data, filters }) {
  // 重い計算をメモ化
  const processedData = useMemo(() => {
    console.log('Processing data...');
    return data
      .filter(item => filters.includes(item.category))
      .map(item => ({
        ...item,
        processedValue: expensiveCalculation(item.value)
      }))
      .sort((a, b) => b.processedValue - a.processedValue);
  }, [data, filters]);

  // 複雑な統計計算をメモ化
  const statistics = useMemo(() => {
    console.log('Calculating statistics...');
    return {
      total: processedData.reduce((sum, item) => sum + item.processedValue, 0),
      average: processedData.length > 0 
        ? processedData.reduce((sum, item) => sum + item.processedValue, 0) / processedData.length 
        : 0,
      max: Math.max(...processedData.map(item => item.processedValue)),
      min: Math.min(...processedData.map(item => item.processedValue))
    };
  }, [processedData]);

  return (
    <div>
      <div className="statistics">
        <p>合計: {statistics.total}</p>
        <p>平均: {statistics.average.toFixed(2)}</p>
        <p>最大: {statistics.max}</p>
        <p>最小: {statistics.min}</p>
      </div>
      <div className="data-list">
        {processedData.map(item => (
          <div key={item.id} className="data-item">
            <h3>{item.name}</h3>
            <p>値: {item.processedValue}</p>
          </div>
        ))}
      </div>
    </div>
  );
}

function expensiveCalculation(value) {
  // 重い計算のシミュレーション
  let result = value;
  for (let i = 0; i < 1000000; i++) {
    result = Math.sqrt(result + i);
  }
  return result;
}

useCallback

import { useCallback, useState, memo } from 'react';

// 子コンポーネント
const ListItem = memo(({ item, onUpdate, onDelete }) => {
  console.log(`ListItem ${item.id} rendered`);
  
  return (
    <div className="list-item">
      <h3>{item.title}</h3>
      <p>{item.description}</p>
      <button onClick={() => onUpdate(item.id, { title: 'Updated' })}>
        更新
      </button>
      <button onClick={() => onDelete(item.id)}>
        削除
      </button>
    </div>
  );
});

// 親コンポーネント
function ItemList({ items }) {
  const [selectedItems, setSelectedItems] = useState([]);

  // コールバック関数をメモ化
  const handleUpdate = useCallback((id, updates) => {
    console.log(`Updating item ${id}`, updates);
    // API呼び出しなどの処理
  }, []);

  const handleDelete = useCallback((id) => {
    console.log(`Deleting item ${id}`);
    // 削除処理
  }, []);

  const handleSelect = useCallback((id) => {
    setSelectedItems(prev => 
      prev.includes(id) 
        ? prev.filter(itemId => itemId !== id)
        : [...prev, id]
    );
  }, []);

  return (
    <div>
      <h2>アイテムリスト</h2>
      {items.map(item => (
        <ListItem
          key={item.id}
          item={item}
          onUpdate={handleUpdate}
          onDelete={handleDelete}
          onSelect={handleSelect}
        />
      ))}
    </div>
  );
}

仮想化とレイジーローディング

React Window

import { FixedSizeList as List } from 'react-window';

// 大量のデータを効率的に表示
function VirtualizedList({ items }) {
  const Row = ({ index, style }) => (
    <div style={style} className="list-row">
      <h3>{items[index].title}</h3>
      <p>{items[index].description}</p>
    </div>
  );

  return (
    <List
      height={600}
      itemCount={items.length}
      itemSize={100}
      width="100%"
    >
      {Row}
    </List>
  );
}

// 可変サイズのリスト
import { VariableSizeList as VariableList } from 'react-window';

function VariableSizedList({ items }) {
  const getItemSize = (index) => {
    // アイテムのサイズを動的に計算
    const item = items[index];
    return item.type === 'header' ? 60 : 40;
  };

  const Row = ({ index, style }) => (
    <div style={style} className={`list-row ${items[index].type}`}>
      {items[index].content}
    </div>
  );

  return (
    <VariableList
      height={600}
      itemCount={items.length}
      itemSize={getItemSize}
      width="100%"
    >
      {Row}
    </VariableList>
  );
}

Intersection Observer

import { useState, useEffect, useRef } from 'react';

// 無限スクロールの実装
function useInfiniteScroll(fetchMore, hasMore) {
  const [isFetching, setIsFetching] = useState(false);
  const observerRef = useRef();

  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting && hasMore && !isFetching) {
          setIsFetching(true);
          fetchMore().finally(() => setIsFetching(false));
        }
      },
      { threshold: 1.0 }
    );

    if (observerRef.current) {
      observer.observe(observerRef.current);
    }

    return () => observer.disconnect();
  }, [fetchMore, hasMore, isFetching]);

  return [observerRef, isFetching];
}

function InfiniteScrollList() {
  const [items, setItems] = useState([]);
  const [hasMore, setHasMore] = useState(true);

  const fetchMore = async () => {
    // API呼び出し
    const newItems = await fetchItems(items.length, 20);
    setItems(prev => [...prev, ...newItems]);
    setHasMore(newItems.length === 20);
  };

  const [observerRef, isFetching] = useInfiniteScroll(fetchMore, hasMore);

  return (
    <div>
      {items.map(item => (
        <div key={item.id} className="item">
          {item.content}
        </div>
      ))}
      <div ref={observerRef}>
        {isFetching && <p>読み込み中...</p>}
      </div>
    </div>
  );
}

Code Splitting

React.lazy

import { Suspense, lazy } from 'react';

// 動的インポート
const Dashboard = lazy(() => import('./Dashboard'));
const Profile = lazy(() => import('./Profile'));
const Settings = lazy(() => import('./Settings'));

// エラーバウンダリー付きの遅延読み込み
const LazyComponent = lazy(() => 
  import('./HeavyComponent').catch(() => ({
    default: () => <div>コンポーネントの読み込みに失敗しました</div>
  }))
);

function App() {
  return (
    <Router>
      <Suspense fallback={<div>読み込み中...</div>}>
        <Routes>
          <Route path="/dashboard" element={<Dashboard />} />
          <Route path="/profile" element={<Profile />} />
          <Route path="/settings" element={<Settings />} />
        </Routes>
      </Suspense>
    </Router>
  );
}

動的インポート

// 条件付きコンポーネント読み込み
function ConditionalComponent({ shouldLoadHeavyComponent }) {
  const [HeavyComponent, setHeavyComponent] = useState(null);

  useEffect(() => {
    if (shouldLoadHeavyComponent && !HeavyComponent) {
      import('./HeavyComponent').then(module => {
        setHeavyComponent(() => module.default);
      });
    }
  }, [shouldLoadHeavyComponent, HeavyComponent]);

  if (!shouldLoadHeavyComponent) {
    return <div>軽量なコンテンツ</div>;
  }

  if (!HeavyComponent) {
    return <div>読み込み中...</div>;
  }

  return <HeavyComponent />;
}

// ライブラリの動的読み込み
function ChartComponent({ data }) {
  const [Chart, setChart] = useState(null);

  useEffect(() => {
    // Chart.jsを動的に読み込み
    Promise.all([
      import('chart.js'),
      import('react-chartjs-2')
    ]).then(([chartjs, reactChartjs]) => {
      setChart(() => reactChartjs.Line);
    });
  }, []);

  if (!Chart) {
    return <div>チャートを読み込み中...</div>;
  }

  return <Chart data={data} />;
}

状態管理の最適化

Context の分割

// 複数のContextに分割
const UserContext = createContext();
const ThemeContext = createContext();
const NotificationContext = createContext();

// 必要な部分のみを更新
function UserProvider({ children }) {
  const [user, setUser] = useState(null);
  
  const value = useMemo(() => ({ user, setUser }), [user]);
  
  return (
    <UserContext.Provider value={value}>
      {children}
    </UserContext.Provider>
  );
}

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  
  const value = useMemo(() => ({ theme, setTheme }), [theme]);
  
  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
}

// カスタムフック
function useUser() {
  const context = useContext(UserContext);
  if (!context) {
    throw new Error('useUser must be used within UserProvider');
  }
  return context;
}

function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider');
  }
  return context;
}

useReducer の活用

// 複雑な状態管理
const initialState = {
  items: [],
  loading: false,
  error: null,
  filters: {
    category: 'all',
    sortBy: 'name',
    searchTerm: ''
  }
};

function itemsReducer(state, action) {
  switch (action.type) {
    case 'FETCH_START':
      return { ...state, loading: true, error: null };
    
    case 'FETCH_SUCCESS':
      return { 
        ...state, 
        loading: false, 
        items: action.payload,
        error: null 
      };
    
    case 'FETCH_ERROR':
      return { 
        ...state, 
        loading: false, 
        error: action.payload 
      };
    
    case 'UPDATE_FILTER':
      return {
        ...state,
        filters: { ...state.filters, [action.key]: action.value }
      };
    
    case 'ADD_ITEM':
      return {
        ...state,
        items: [...state.items, action.payload]
      };
    
    case 'UPDATE_ITEM':
      return {
        ...state,
        items: state.items.map(item =>
          item.id === action.payload.id ? action.payload : item
        )
      };
    
    case 'DELETE_ITEM':
      return {
        ...state,
        items: state.items.filter(item => item.id !== action.id)
      };
    
    default:
      return state;
  }
}

function ItemManager() {
  const [state, dispatch] = useReducer(itemsReducer, initialState);

  const fetchItems = useCallback(async () => {
    dispatch({ type: 'FETCH_START' });
    try {
      const items = await api.getItems(state.filters);
      dispatch({ type: 'FETCH_SUCCESS', payload: items });
    } catch (error) {
      dispatch({ type: 'FETCH_ERROR', payload: error.message });
    }
  }, [state.filters]);

  const updateFilter = useCallback((key, value) => {
    dispatch({ type: 'UPDATE_FILTER', key, value });
  }, []);

  return (
    <div>
      <FilterControls 
        filters={state.filters} 
        onFilterChange={updateFilter} 
      />
      {state.loading && <div>読み込み中...</div>}
      {state.error && <div>エラー: {state.error}</div>}
      <ItemList items={state.items} />
    </div>
  );
}

バンドルサイズの最適化

Tree Shaking

// ❌ 全体をインポート
import * as _ from 'lodash';
import { Button, TextField, Dialog } from '@mui/material';

// ✅ 必要な部分のみインポート
import debounce from 'lodash/debounce';
import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField';

// ✅ 名前付きインポート
import { debounce } from 'lodash-es';

Webpack Bundle Analyzer

// webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: 'static',
      openAnalyzer: false,
      reportFilename: 'bundle-report.html'
    })
  ]
};

// package.json
{
  "scripts": {
    "analyze": "npm run build && npx webpack-bundle-analyzer build/static/js/*.js"
  }
}

まとめ

Reactアプリケーションのパフォーマンス最適化のポイント:

  1. 測定から始める: React DevTools Profilerを活用
  2. メモ化を適切に使用: React.memo、useMemo、useCallback
  3. 仮想化: 大量のデータを効率的に表示
  4. Code Splitting: 必要な時に必要なコードを読み込み
  5. 状態管理の最適化: Contextの分割、useReducerの活用
  6. バンドルサイズの最適化: Tree Shaking、不要なライブラリの削除

継続的な測定と改善により、ユーザーエクスペリエンスを向上させましょう!

頑張って! 🚀

Related Posts