React: パフォーマンスの最適化:useMemo と useCallback
最終更新:2026-10-26
1. 学習内容
- useMemo は計算結果をキャッシュします
- useCallback は関数の参照をキャッシュします
- React.memo は不要な再レンダリングを防ぎます
- いつ使うべきか、いつ使いすぎるべきか
- パフォーマンス分析の手法
2. 検索フィルターページの物語
パフォーマンス最適化の3つの要素間の関係
flowchart TD
A[Parent Component Rendering] --> B{Props It's changed?}
B -->|Yes| C[React.memo Intercept?]
B -->|No| D[Skip subcomponents ✅]
C -->|A rough comparison is roughly equal| D
C -->|Shallow Comparison Inequality| E{Function/The object is new.?}
E -->|Yes| F[useCallback/useMemo]
F --> G[Stable Citation → memo Effective ✅]
E -->|No| D
style F fill:#e8f5e9,stroke:#2e7d32
style D fill:#c8e6c9,stroke:#2e7d32
style G fill:#c8e6c9,stroke:#2e7d32
(1) 課題:入力ごとに再計算が必要
アリスは、10,000点の商品を絞り込み検索できる商品検索ページを作成しました:
JSX
function ShopPage() {
const [products] = useState(generate10000Products())
const [search, setSearch] = useState('')
const [sortBy, setSortBy] = useState('name')
// ❌ Question:Re-filter after each input 10,000 Items
const filtered = products
.filter(p => p.name.includes(search))
.sort((a, b) => a[sortBy] > b[sortBy] ? 1 : -1)
return (
<div>
<input value={search} onChange={e => setSearch(e.target.value)} />
<select value={sortBy} onChange={e => setSortBy(e.target.value)}>
<option value="name">By Name</option>
<option value="price">By Price</option>
</select>
<ProductList products={filtered} />
</div>
)
}
問題:キーボード入力(setSearch)が行われるたびに、filterとsortが再実行されてしまい、そのたびに10,000件のレコードをフィルタリングする必要があるため、インターフェースの動作が遅延してしまう。
(2) useMemo ソリューション
JSX
function ShopPage() {
const [products] = useState(generate10000Products())
const [search, setSearch] = useState('')
const [sortBy, setSortBy] = useState('name')
// ✅ useMemo:Only at search or sortBy Recalculate only when there is a change
const filtered = useMemo(() => {
console.log('Refilter...(Only when the search terms or sorting criteria change)')
return products
.filter(p => p.name.includes(search))
.sort((a, b) => a[sortBy] > b[sortBy] ? 1 : -1)
}, [search, sortBy, products])
return (
<div>
<input value={search} onChange={e => setSearch(e.target.value)} />
<select value={sortBy} onChange={e => setSortBy(e.target.value)}>
<option value="name">By Name</option>
<option value="price">By Price</option>
</select>
<ProductList products={filtered} />
</div>
)
}
メリット:フィルタリングは、入力が search または sortBy に変化した場合にのみ再実行されます。その他の状態変化(親コンポーネントの再レンダリングなど)ではフィルタリングはトリガーされないため、10~100倍のパフォーマンス向上が実現されます。
3. useMemo:計算結果のキャッシュ
| フック | 留意点 | 戻り値 | 依存関係が変更された場合 |
|---|---|---|---|
useMemo |
計算結果 | キャッシュされた値 | 再計算 |
useCallback |
関数リファレンス | キャッシュされた関数 | 新しい関数の作成 |
React.memo |
コンポーネント全体のレンダリング結果 | 再レンダリングをスキップ | props が変更されたときに再レンダリング |
(1) 基本的な構文
JSX
const memoizedValue = useMemo(() => {
return Costly Computations()
}, [Dependency1, Dependency2])
| セクション | 説明 |
|---|---|
() => Computation |
キャッシュする必要があり、計算結果を返す関数 |
[Dependencies] |
依存関係が変更された際に再計算する |
memoizedValue |
キャッシュされた結果(依存関係に変更がない場合は、前回の結果を直接返す) |
(2) 適用されるシナリオ
JSX
// Scene 1:Data Conversion(Filter、Sort、Grouping)
const activeUsers = useMemo(
() => users.filter(u => u.isActive).sort((a, b) => a.name.localeCompare(b.name)),
[users]
)
// Scene 2:Format(Numbers、Date、String Concatenation)
const formattedPrice = useMemo(
() => new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY' }).format(price),
[price]
)
// Scene 3:Derived State(Calculating New Data from Existing Data)
const statistics = useMemo(() => ({
total: orders.length,
paid: orders.filter(o => o.status === 'paid').length,
amount: orders.reduce((sum, o) => sum + o.amount, 0),
avgAmount: orders.length ? orders.reduce((sum, o) => sum + o.amount, 0) / orders.length : 0
}), [orders])
出力:
TEXT
...
▶ サンプル:性能比較
JSX
// ============================================
// Example:useMemo vs no useMemo Performance Comparison
// ============================================
function ExpensiveCalc({ num, unrelated }) {
// ❌ None useMemo:Executed on every render
const slowResult1 = slowFibonacci(num)
// ✅ With useMemo: num If it hasn't changed, don't recalculate it.
const slowResult2 = useMemo(() => slowFibonacci(num), [num])
return (
<div>
<p>num={num} The Fibonacci sequence</p>
<p>Without useMemo: {slowResult1}(Rendering calculations each time)</p>
<p>With useMemo: {slowResult2}(Only at num Calculate when changes occur)</p>
<p>Unrelated Status:{unrelated}(Click the button to change this value)</p>
</div>
)
}
function slowFibonacci(n) {
if (n <= 1) return n
let a = 0, b = 1, c
for (let i = 2; i <= n; i++) { c = a + b; a = b; b = c }
console.log('fibonacci Calculation!n=' + n)
return b
}
// Click to Make a Difference unrelated → without useMemo recalculates,with useMemo skips
出力:
TEXT
Fibonacci(n):useMemo を使わないとレンダリングのたびに再計算され、useMemo を使うと無関係な状態変化時のみスキップされる。コンソール:"fibonacci calculated! n=X"
4. useCallback:関数参照の記憶
(1) 基本的な構文
JSX
const memoizedFn = useCallback(() => {
doSomething(a, b)
}, [a, b])
useCallback(fn, deps) は useMemo(() => fn, deps) と同等です。格納されるのは、関数の結果ではなく、関数そのものです。
(2) なぜ useCallback が必要なのでしょうか?
JSX
function Parent() {
const [count, setCount] = useState(0)
// ❌ Every time Parent Rendering,handleClick These are all new functions
// resulting in Child 's React.memo Failure
const handleClick = () => {
console.log('Clicked')
}
// ✅ useCallback:handleClick Citation unchanged
// Child 's React.memo Works properly(Skip Re-rendering)
const handleClick = useCallback(() => {
console.log('Clicked')
}, []) // Empty Dependency:This function will never change.
}
(3) React.memo:子コンポーネントの再レンダリングを防ぐ
▶ 試してみよう5. いつ使うべきか、いつ使うべきでないか
(1) いつ使うか
| シナリオ | フック | 理由 |
|---|---|---|
| ビッグデータのフィルタリング/ソート | useMemo |
レンダリングのたびに大量のデータを走査する必要を回避する |
| 複雑な計算(数学・書式設定) | useMemo |
結果が変わらない場合は再計算する必要はありません |
| React.memo の子コンポーネントへのプロパティの受け渡し | useCallback |
React.memo が正常に動作するように関数参照を安定させる |
| useEffect の関数依存関係 | useCallback |
関数の参照変更による useEffect の複数回実行を防ぐ |
(2) 必要とされない状況
JSX
// ❌ Not necessary:Simple Calculations
const doubled = useMemo(() => count * 2, [count])
// ✅ Write it directly:count * 2 It's already pretty fast as it is
const doubled = count * 2
// ❌ Not necessary:Handle events without passing them to child components
const handleClick = useCallback(() => {
console.log('Click')
}, [])
// ✅ Just write it down.
const handleClick = () => console.log('Click')
// ❌ Not necessary:The child component isn't being used. React.memo
// useCallback It's been packaged, but no one is buying it."Stable Citation"
(3) 乱用の代償
JSX
// useMemo and useCallback It involves expenses(Relies heavily on arrays)
// Abuse = Introducing Additional Comparison Logic,Slower than if it weren't optimized
// Rule of thumb:
// 1. First, let's write the version without optimization.
// 2. Feeling some lag(fps < 60)Use again
// 3. use React DevTools Profiler Measurement
6. 完全な例:チャットアプリのメッセージ一覧
▶ 実行例を展開(112 行)
出力:
TEXT
... ('fibonacci Calculation!n=' + n)
期待される出力:100件のメッセージを含むチャットリスト。「未読数を増やす」をクリックしても、
useCallbackおよびReact.memoの影響により、メッセージリストは再描画されず(コンソールに出力は表示されない)、未読数のみが変更されます。
▶ サンプル:useCallback により、useEffect の複数回実行が防止される
▶ 試してみよう出力:
TEXT
検索ボックス + ページネーション。"React" を入力 → 3 件の結果("Results React - Page 1" など)。useCallback で useEffect の無限ループを防止。Prev/Next ボタン。
出力:
TEXT
サブタイトル:"🔍 Search(useCallback Demo)"。表示:"🔍 Search(useCallback Demo)"。状態:query(setter:setQuery)、results(setter:setResults)、page(setter:setPage)。ボタン:setPage(p => p + 1)}>Next Page。入力:Enter a search term....。リスト:{r.title}。useEffect が副作用を管理。useCallback でハンドラ関数の参照をキャッシュ。非同期データ取得/読み込み状態
▶ サンプル:useMemo を使用した仮想リスト計算の実装
JSX
function VirtualList({ items, itemHeight = 40, visibleHeight = 400 }) {
const [scrollTop, setScrollTop] = useState(0)
const visibleCount = Math.ceil(visibleHeight / itemHeight)
const totalHeight = useMemo(() => items.length * itemHeight, [items.length, itemHeight])
const { startIndex, endIndex, offsetY } = useMemo(() => {
const start = Math.floor(scrollTop / itemHeight)
const end = Math.min(start + visibleCount + 1, items.length)
return { startIndex: start, endIndex: end, offsetY: start * itemHeight }
}, [scrollTop, itemHeight, visibleCount, items.length])
const visibleItems = useMemo(() => items.slice(startIndex, endIndex), [items, startIndex, endIndex])
return (
<div onScroll={e => setScrollTop(e.currentTarget.scrollTop)}
style={{ height: visibleHeight, overflowY: 'auto', border: '1px solid #ddd', borderRadius: 4 }}>
<div style={{ height: totalHeight, position: 'relative' }}>
<div style={{ position: 'absolute', top: offsetY, width: '100%' }}>
{visibleItems.map((item, i) => (
<div key={startIndex + i} style={{ height: itemHeight, padding: '0 12px', display: 'flex', alignItems: 'center', borderBottom: '1px solid #f0f0f0' }}>
Row {startIndex + i}: {item}
</div>
))}
</div>
</div>
</div>
)
}
出力:
TEXT
仮想スクロール:可視の行のみレンダリング(1000+ 行中約 10 行)。スクロール → 行が差し替わる。総高さ = items × 40px。useMemo で可視範囲を計算。
出力:
TEXT
表示:"items.slice(startIndex, endIndex), [items, startIndex, endIn"。状態:scrollTop(setter:setScrollTop)。useMemo で計算を最適化
▶ サンプル:React.memo と useCallback を使ったリストの最適化
▶ 試してみよう出力:
TEXT
商品リスト(Keyboard $79、Mouse $49、Monitor $399、Headphones $129)にフィルタ機能。"Remove" で該当項目を削除。React.memo + useCallback = 変化した項目だけが再レンダリングされる。
出力:
TEXT
レンダリング:...
▶ サンプル:useMemo を使用すべきでない場合――アンチパターンの比較
JSX
function BadExample({ count }) {
// ❌ Simple calculations are not required useMemo——Create memo Expenses > The Calculation Itself
const doubled = useMemo(() => count * 2, [count])
// ✅ Direct Calculation
const doubledOk = count * 2
// ❌ Each render creates a new object as useMemo Dependency
const config = useMemo(() => ({ count }), [count])
const badResult = useMemo(() => config.count * 3, [config]) // config A new reference each time!
// ✅ Using Primitive Types Directly as Dependencies
const goodResult = useMemo(() => count * 3, [count])
return <p>Doubled: {doubledOk}, Tripled: {goodResult}</p>
}
function GoodExample({ items, query }) {
// ✅ Filter/Sorting large amounts of data useMemo
const filtered = useMemo(() => {
return items.filter(item =>
item.name.toLowerCase().includes(query.toLowerCase())
).sort((a, b) => a.name.localeCompare(b.name))
}, [items, query])
// ✅ Complex objects passed to child components use useMemo
const chartData = useMemo(() => ({
labels: filtered.map(i => i.name),
values: filtered.map(i => i.price),
}), [filtered])
return (
<div>
<p>Found: {filtered.length} items</p>
<Chart data={chartData} />
</div>
)
}
出力:
TEXT
❌ count*2 に useMemo を使うのはNG(オーバーヘッドが計算自体より大きい)。❌ 依存に新しいオブジェクトを使うと memo が壊れる。✅ 大規模なフィルタ/ソートや派生したグラフデータオブジェクトには useMemo を使う。
❓ よくある質問
Q
useMemo や useCallback は常にパフォーマンスを向上させるのでしょうか?A 必ずしもそうとは限りません。これらには独自のオーバーヘッドがあり、レンダリングのたびに
dependencies 配列と比較処理が行われます。使いすぎると、かえって処理が遅くなる可能性があります。Reactの公式な推奨事項:まずは最適化されていないバージョンを記述し、遅延に気づいた場合にのみ最適化を行うこと。経験則として、React.memo子コンポーネントに渡される計算、またはオーバーヘッドが1 msを超える計算にのみこれらを使用してください。Q
useMemoと通常の変数の違いは何ですか?A
useMemoには「記憶」機能があります。依存関係に変更がない場合、式を再評価することなく、キャッシュされた値を直接使用します。通常の変数は、レンダリングのたびに再評価されます。しかし、useMemoはレンダリング中に依存関係の配列を比較する必要があるため、「単純な計算では、実際にはuseMemoの方が処理が遅くなります。」Q 関数の参照を保持するために、いつ
useCallback を使用すべきですか?A 以下の2つの条件の両方が満たされる場合にのみ使用する必要があります:① その関数が React.memo の子コンポーネントにプロパティとして渡されていること;② その関数が
useEffect フックの dependencies 配列に含まれていること。これらの条件のいずれも当てはまらない場合は、関数を直接記述するだけでよく、useCallback は不要です。Q
useMemo と useCallback の違いは何ですか?A
useMemo(() => value, deps) は計算結果(値)をキャッシュするのに対し、useCallback(() => fn, deps) は関数参照をキャッシュします。実際、useCallback(fn, deps) は useMemo(() => fn, deps) と同等です。子コンポーネントに渡される関数をキャッシュする必要がある場合は useCallback を、計算結果をキャッシュする必要がある場合は useMemo を使用してください。📖 まとめ
useMemo計算結果をキャッシュする。依存関係に変更がない場合は、再計算を行わないuseCallbackReact.memo と連携して、子コンポーネントが再レンダリングされないように、関数への参照をキャッシュするReact.memoプロパティの浅い比較を行い、プロパティに変更がない場合は、子コンポーネントのレンダリングをスキップする- 早すぎる最適化は避けること。まずは正常に動作するコードを書き、動作が重くなってから最適化を行うようにする。
- React DevToolsのプロファイラを使ったパフォーマンスのボトルネックの特定
📝 練習問題
- 基本問題(難易度 ⭐):整数 n を入力として受け取り、n 番目の素数を計算する
PrimeCalculatorコンポーネントを作成してください。新しい値が入力されるたびに再計算されないよう、useMemoを使用して結果をキャッシュしてください。 - 上級演習(難易度 ⭐⭐):500件の商品レコードを含み、カテゴリによるフィルタリングと価格によるソートをサポートする
ProductTableコンポーネントを作成してください。useMemoを使用してフィルタリングおよびソートのロジックを最適化し、useCallbackとReact.memoを組み合わせてリストのレンダリングを最適化してください。 - 課題(難易度:⭐⭐⭐):3つの異なるディメンション(時間、地域、カテゴリ)にわたる10,000件の注文レコードを集計する
DataDashboardコンポーネントを作成し、useMemoを使用して階層型キャッシュを実装してください(ディメンションが変更された際に、関連する階層のみが再計算されるようにするため)。