Reactアプリケーションのパフォーマンスを最適化するには、不要な再レンダリングを防ぐことが重要です。React.memo、useMemo、useCallbackを使い分けることで、効率的なレンダリングを実現できます。ここでは、それぞれの使い方と最適化のポイントを解説します。
基本的な使い方
sample.jsx
import React, { useState, useCallback, useMemo, memo } from 'react';
// 最適化された子コンポーネント(メモ化)
const ChildComponent = memo(function ChildComponent({ name, count, onClick }) {
console.log(`${name} コンポーネントがレンダリングされました`);
return (
<div style={{
border: '1px solid #ccc',
borderRadius: '8px',
padding: '15px',
margin: '10px 0',
backgroundColor: '#f9f9f9'
}}>
<h3>{name}</h3>
<p>カウント: {count}</p>
<button
onClick={onClick}
style={{
padding: '8px 16px',
backgroundColor: '#4CAF50',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
ボタンをクリック
</button>
</div>
);
});
// 高コストな計算を行う関数
function expensiveCalculation(num) {
console.log('高コストな計算を実行中...');
// 時間のかかる処理をシミュレート
let result = 0;
for (let i = 0; i < 1000000; i++) {
result += num;
}
return result;
}
// メイン(親)コンポーネント
function PerformanceDemo() {
// 状態
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
const [inputValue, setInputValue] = useState('');
// イベントハンドラー(最適化なし)
const handleClickNonOptimized = () => {
console.log('最適化なしボタンがクリックされました');
setCount1(count1 + 1);
};
// イベントハンドラー(useCallbackで最適化)
const handleClickOptimized = useCallback(() => {
console.log('最適化済みボタンがクリックされました');
setCount2(count2 + 1);
}, [count2]); // count2が変わったときだけ関数を再作成
// 高コストな計算(最適化なし)
const expensiveResultNonOptimized = expensiveCalculation(count1);
// 高コストな計算(useMemoで最適化)
const expensiveResultOptimized = useMemo(() => {
return expensiveCalculation(count2);
}, [count2]); // count2が変わったときだけ再計算
return (
<div style={{
fontFamily: 'Arial, sans-serif',
maxWidth: '800px',
margin: '0 auto',
padding: '20px'
}}>
<h1>Reactのパフォーマンス最適化</h1>
<div style={{ marginBottom: '20px' }}>
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="テキストを入力してみてください"
style={{
padding: '8px',
width: '100%',
boxSizing: 'border-box',
marginBottom: '10px'
}}
/>
<p>入力値: {inputValue}</p>
<p><strong>注意:</strong> テキストを入力すると親コンポーネントが再レンダリングされますが、
メモ化された子コンポーネントはpropsが変わらない限り再レンダリングされません。</p>
</div>
<div style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: '20px',
marginBottom: '30px'
}}>
{/* 最適化なしのセクション */}
<div style={{
border: '1px solid #e0e0e0',
borderRadius: '8px',
padding: '20px',
backgroundColor: '#fff8f8'
}}>
<h2>最適化なし</h2>
<p>計算結果: {expensiveResultNonOptimized}</p>
<ChildComponent
name="最適化なし"
count={count1}
onClick={handleClickNonOptimized}
/>
<p>テキストボックスに入力するたびに、このコンポーネントの関数は再作成され、
高コストな計算も毎回実行されます。</p>
</div>
{/* 最適化ありのセクション */}
<div style={{
border: '1px solid #e0e0e0',
borderRadius: '8px',
padding: '20px',
backgroundColor: '#f0fff0'
}}>
<h2>最適化あり</h2>
<p>計算結果: {expensiveResultOptimized}</p>
<ChildComponent
name="最適化あり"
count={count2}
onClick={handleClickOptimized}
/>
<p>useCallback と useMemo を使用しているため、count2が変わらない限り
関数の再作成や高コストな計算は行われません。</p>
</div>
</div>
<div style={{
backgroundColor: '#e7f4fd',
padding: '15px',
borderRadius: '8px',
marginBottom: '20px'
}}>
<h2>パフォーマンス改善のポイント</h2>
<ul>
<li><strong>React.memo</strong> - 不要な再レンダリングを防止</li>
<li><strong>useCallback</strong> - 関数のインスタンスをメモ化</li>
<li><strong>useMemo</strong> - 計算結果をメモ化</li>
<li><strong>コンソールでのログ確認</strong> - ブラウザのコンソールを開いて、いつコンポーネントが再レンダリングされるかを確認してください</li>
</ul>
</div>
<p style={{ fontSize: '14px', color: '#666' }}>
このデモでは、テキストボックスに入力するたびに親コンポーネントが再レンダリングされます。
しかし、「最適化あり」の子コンポーネントは、関連するカウントが変わらない限り再レンダリングされません。
「最適化なし」の子コンポーネントは、親が再レンダリングされるたびに再レンダリングされます。
</p>
</div>
);
}
export default PerformanceDemo;
説明
Step 1Reactアプリのパフォーマンス問題
React アプリケーションでは、以下のような一般的なパフォーマンス問題が発生することがあります:
- 不要な再レンダリング:コンポーネントが必要以上に再レンダリングされる
- 大きなバンドルサイズ:ページの読み込み時間が長くなる
- メモリリーク:不適切なクリーンアップによるメモリの消費
- 非効率な状態管理:過剰な状態の更新や不適切な状態設計
これらの問題を解決するために、Reactはいくつかの最適化ツールを提供しています。
Step 2React.memo による不要な再レンダリングの防止
React.memoは、propsが変更されない限りコンポーネントの再レンダリングをスキップする高階コンポーネントです。
import React from 'react'; // 通常のコンポーネント function ExpensiveComponent({ name, count }) { console.log(`${name} コンポーネントがレンダリングされました`); // 重い計算を模倣 let result = 0; for (let i = 0; i < 1000000; i++) { result += i; } return ( <div> <p>名前: {name}</p> <p>カウント: {count}</p> <p>計算結果: {result}</p> </div> ); } // React.memo でラップしたバージョン const MemoizedExpensiveComponent = React.memo(ExpensiveComponent); // 親コンポーネント function ParentComponent() { const [count1, setCount1] = React.useState(0); const [count2, setCount2] = React.useState(0); return ( <div> <h1>React.memo の例</h1> <div> <h2>通常のコンポーネント(常に再レンダリング)</h2> <ExpensiveComponent name="通常" count={count1} /> <button onClick={() => setCount1(count1 + 1)}> Count1 を増やす (+{count1}) </button> </div> <div> <h2>メモ化されたコンポーネント(propsが変わると再レンダリング)</h2> <MemoizedExpensiveComponent name="メモ化" count={count2} /> <button onClick={() => setCount2(count2 + 1)}> Count2 を増やす (+{count2}) </button> </div> <p> いずれかのボタンをクリックすると、通常のコンポーネントは常に再レンダリングされますが、 メモ化されたコンポーネントは自身のpropsが変わった場合にのみ再レンダリングされます。 </p> </div> ); }
React.memo はデフォルトでは浅い比較を行いますが、カスタム比較関数を提供することもできます:
const MemoizedComponent = React.memo( MyComponent, (prevProps, nextProps) => { // true を返すと再レンダリングをスキップ // false を返すと再レンダリングを行う return prevProps.importantProp === nextProps.importantProp; } );
Step 3useMemo と useCallback による値と関数のメモ化
useMemo は計算コストの高い値の再計算を防ぎます:
import React, { useState, useMemo } from 'react'; function ExpensiveCalculation({ numbers }) { // useMemo で計算結果をメモ化 const total = useMemo(() => { console.log('高コストな計算を実行中...'); return numbers.reduce((acc, num) => acc + num, 0); }, [numbers]); // numbers が変わった場合のみ再計算 return <div>合計: {total}</div>; }
useCallback は関数のインスタンスをメモ化し、子コンポーネントへのpropsとして渡す際に役立ちます:
import React, { useState, useCallback } from 'react'; function ParentComponent() { const [count, setCount] = useState(0); const [text, setText] = useState(''); // useCallback を使わない場合、handleClick は再レンダリングごとに新しい関数になる const handleClickWithoutCallback = () => { console.log('Clicked!', count); }; // useCallback を使うと、依存配列の値が変わらない限り同じ関数インスタンスが保持される const handleClickWithCallback = useCallback(() => { console.log('Clicked!', count); }, [count]); // count が変わった場合のみ関数が再作成される return ( <div> <input value={text} onChange={(e) => setText(e.target.value)} placeholder="テキストを入力" /> <p>入力テキスト: {text}</p> <p>カウント: {count}</p> <button onClick={() => setCount(count + 1)}> カウント増加 </button> {/* text が変わるたびに新しい handleClickWithoutCallback が作成されるため、 React.memo を使っていても ChildComponent は再レンダリングされる */} <ChildComponent onClick={handleClickWithoutCallback} name="最適化なし" /> {/* count が変わらない限り同じ handleClickWithCallback が使われるため、 React.memo を使った場合、text が変わっても ChildComponent は再レンダリングされない */} <ChildComponent onClick={handleClickWithCallback} name="useCallback" /> </div> ); } // React.memo でメモ化した子コンポーネント const ChildComponent = React.memo(function ChildComponent({ onClick, name }) { console.log(`${name} コンポーネントがレンダリングされました`); return ( <button onClick={onClick}> {name} ボタン </button> ); });
Step 4コード分割とLazy Loading
ReactのReact.lazyとSuspenseを使用して、コードを分割し必要な時だけ読み込むことができます:
import React, { Suspense, lazy } from 'react'; import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; // コンポーネントを動的にインポート const Home = lazy(() => import('./pages/Home')); const About = lazy(() => import('./pages/About')); const Contact = lazy(() => import('./pages/Contact')); function App() { return ( <Router> <div> <nav> <ul> <li><a href="/">ホーム</a></li> <li><a href="/about">会社概要</a></li> <li><a href="/contact">お問い合わせ</a></li> </ul> </nav> {/* Suspenseでラップしてローディング状態を処理 */} <Suspense fallback={<div>読み込み中...</div>}> <Routes> <Route path="/" element={<Home />} /> <Route path="/about" element={<About />} /> <Route path="/contact" element={<Contact />} /> </Routes> </Suspense> </div> </Router> ); }
この方法では、アプリケーションの初期ロード時に全てのコードを読み込むのではなく、必要なときに必要なコードだけを読み込むことができます。
Step 5仮想リストの実装
大量のデータを表示する場合、全ての要素をレンダリングするのではなく、画面に表示されている部分だけをレンダリングする「仮想リスト(Virtualized List)」を使用することで、パフォーマンスを大幅に改善できます。
react-windowやreact-virtualized などのライブラリを使用できます:
// 例: react-window の使用 import React from 'react'; import { FixedSizeList } from 'react-window'; // 大量のデータがある想定 const bigList = Array.from({ length: 10000 }, (_, i) => `項目 ${i + 1}`); function VirtualizedList() { // 各行のレンダリング関数 const Row = ({ index, style }) => ( <div style={style}> {bigList[index]} </div> ); return ( <div> <h1>10,000項目のリスト(仮想化あり)</h1> <FixedSizeList height={400} width={300} itemSize={35} // 各項目の高さ itemCount={bigList.length} > {Row} </FixedSizeList> </div> ); }
パフォーマンス最適化のベストプラクティス:
- パフォーマンス問題が実際に発生してから最適化を行う(早過ぎる最適化は避ける)
- React DevToolsのPerformanceタブを使ってボトルネックを特定する
- 大きなコンポーネントを小さな、焦点を絞ったコンポーネントに分割する
- 状態の更新は必要最小限に抑え、適切な粒度で管理する
- リストをレンダリングする際は常にユニークな「key」プロパティを使用する
- 特にpropsとして関数を渡す場合は useCallback を使用する
- 計算コストの高い処理には useMemo を使用する
- 不要なイベントリスナーやタイマーは useEffect のクリーンアップ関数で解除する
まとめ
React.memoはコンポーネントをメモ化し、Propsが変わらない限り再レンダリングをスキップするuseMemoは計算結果をキャッシュし、依存値が変わらない限り再計算を防ぐuseCallbackは関数の参照をキャッシュし、子コンポーネントへの不要な再レンダリングを防ぐ- 最適化は計測してボトルネックを特定してから適用する(React DevToolsのProfilerが便利)
- すべてのコンポーネントをメモ化する必要はなく、パフォーマンス問題がある箇所に集中する
- stateの構造を適切に設計し、影響範囲を最小化するのも重要な最適化手法