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アプリケーションのパフォーマンス最適化のポイント:
- 測定から始める: React DevTools Profilerを活用
- メモ化を適切に使用: React.memo、useMemo、useCallback
- 仮想化: 大量のデータを効率的に表示
- Code Splitting: 必要な時に必要なコードを読み込み
- 状態管理の最適化: Contextの分割、useReducerの活用
- バンドルサイズの最適化: Tree Shaking、不要なライブラリの削除
継続的な測定と改善により、ユーザーエクスペリエンスを向上させましょう!
頑張って! 🚀
