Reactアプリケーションが大きくなるにつれて、パフォーマンスが重要になります。
不要な再レンダリングを防ぐことで、アプリケーションを高速化できます。
この記事では、React.memo、useMemo、useCallbackを使った最適化の方法を詳しく学んでいきます。
Contents
Reactの再レンダリング
このセクションでは、Reactがいつ・なぜ再レンダリングを行うかの仕組みを理解できます。
「なんとなく動いている」状態から脱却し、パフォーマンス問題の原因を自分で特定できるようになります。
再レンダリングが起きるタイミング
Reactコンポーネントは、以下の4つのタイミングで再レンダリングされます。
これを把握しておくことが、最適化の第一歩です。
- stateが変更されたとき
- propsが変更されたとき
- 親コンポーネントが再レンダリングされたとき
- Contextの値が変更されたとき
問題のある例
親コンポーネントのstateが変化すると、関係のない子コンポーネントも一緒に再レンダリングされてしまいます。
以下はその典型例です。
テキスト入力のたびに重い処理を持つExpensiveComponentが動いてしまうため、UIが詰まる原因になります。
import { useState } from 'react'
// 重い計算をするコンポーネント
function ExpensiveComponent({ value }) {
console.log('ExpensiveComponentが再レンダリング')
// 重い計算(デモ用)
let result = 0
for (let i = 0; i < 1000000000; i++) {
result += i
}
return <div>値: {value}</div>
}
function App() {
const [count, setCount] = useState(0)
const [text, setText] = useState('')
return (
<div>
{/* textが変わるたびにExpensiveComponentも再レンダリング */}
<input value={text} onChange={(e) => setText(e.target.value)} />
<button onClick={() => setCount(count + 1)}>カウント: {count}</button>
<ExpensiveComponent value={count} />
</div>
)
}
この問題を最適化していきます。
React.memo・・・コンポーネント単位で再レンダリングをスキップ
コンポーネント単位で再レンダリングをスキップする方法を習得できます。
このセクションでは、React.memoの基本的な使い方から、カスタム比較関数を使った応用パターンまで身につきます。
基本的な使い方
React.memoでコンポーネントをラップすると、propsが前回と同じ場合に再レンダリングをスキップします。
先ほどの問題例では、textが変化してもExpensiveComponentのprops(value)は変わっていないため、再レンダリングを防げます。
JavaScript版:
import { memo } from 'react'
// memoでラップ
const ExpensiveComponent = memo(function ExpensiveComponent({ value }) {
console.log('ExpensiveComponentが再レンダリング')
let result = 0
for (let i = 0; i < 1000000000; i++) {
result += i
}
return <div>値: {value}</div>
})
function App() {
const [count, setCount] = useState(0)
const [text, setText] = useState('')
return (
<div>
{/* textが変わってもExpensiveComponentは再レンダリングされない */}
<input value={text} onChange={(e) => setText(e.target.value)} />
<button onClick={() => setCount(count + 1)}>カウント: {count}</button>
<ExpensiveComponent value={count} />
</div>
)
}
TypeScript版:
import { memo } from 'react'
interface ExpensiveComponentProps {
value: number
}
const ExpensiveComponent = memo(function ExpensiveComponent({
value
}: ExpensiveComponentProps): JSX.Element {
console.log('ExpensiveComponentが再レンダリング')
let result = 0
for (let i = 0; i < 1000000000; i++) {
result += i
}
return <div>値: {value}</div>
})
カスタム比較関数
デフォルトのReact.memoはpropsを浅い比較(shallow compare)で判定します。
「idが同じならオブジェクトの他のプロパティが変わっても再レンダリングしない」といった独自ルールを設けたい場合は、第2引数にカスタム比較関数を渡します。
trueを返すとスキップ、falseを返すと再レンダリングが実行されます。
JavaScript版:
import { memo } from 'react'
const UserCard = memo(
function UserCard({ user }) {
console.log('UserCardが再レンダリング')
return (
<div>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
)
},
// カスタム比較関数
(prevProps, nextProps) => {
// trueを返すと再レンダリングをスキップ
return prevProps.user.id === nextProps.user.id
}
)
TypeScript版:
import { memo } from 'react'
interface User {
id: number
name: string
email: string
}
interface UserCardProps {
user: User
}
const UserCard = memo<UserCardProps>(
function UserCard({ user }) {
console.log('UserCardが再レンダリング')
return (
<div>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
)
},
(prevProps, nextProps) => {
return prevProps.user.id === nextProps.user.id
}
)
useMemo・・・計算結果をキャッシュし不要な再計算を防ぐ
計算結果をキャッシュして、不要な再計算を防ぐ方法を習得できます。
単純な合計計算から、フィルタリング・ソートといった実務頻出の処理まで、useMemoの効果的な使い所を理解できます。
重い計算をメモ化
useMemoは計算結果をキャッシュするフックです。
依存配列に指定した値が変わったときだけ再計算し、それ以外の再レンダリングではキャッシュされた値を返します。
コンポーネント内で時間のかかる処理(大量データの集計・変換など)を行う場合に有効です。
JavaScript版:
import { useState, useMemo } from 'react'
function ExpensiveCalculation({ numbers }) {
// numbersが変わったときだけ再計算
const sum = useMemo(() => {
console.log('計算中...')
return numbers.reduce((acc, num) => acc + num, 0)
}, [numbers])
return <div>合計: {sum}</div>
}
function App() {
const [numbers] = useState([1, 2, 3, 4, 5])
const [count, setCount] = useState(0)
return (
<div>
{/* countが変わってもsumは再計算されない */}
<button onClick={() => setCount(count + 1)}>
カウント: {count}
</button>
<ExpensiveCalculation numbers={numbers} />
</div>
)
}
TypeScript版:
import { useState, useMemo } from 'react'
interface ExpensiveCalculationProps {
numbers: number[]
}
function ExpensiveCalculation({ numbers }: ExpensiveCalculationProps): JSX.Element {
const sum = useMemo(() => {
console.log('計算中...')
return numbers.reduce((acc, num) => acc + num, 0)
}, [numbers])
return <div>合計: {sum}</div>
}
memo(React.memo)
コンポーネント丸ごとの再レンダリングをスキップします。
useMemo
コンポーネントは再レンダリングされるけれど、その中のuseMemoでラップした特定の計算だけをスキップします。コンポーネント関数は呼ばれるが、依存配列が変わっていなければキャッシュした値をそのまま返します。
フィルタリングとソートの最適化
Todoリストなどで「絞り込み+並び替え」を同時に行う場面は非常によくあります。
これをコンポーネント内にそのまま書くと、無関係なstate変更のたびに毎回実行されてしまいます。
useMemoでまとめてメモ化することで、todos・filter・sortByが変わったときだけ再計算されるようになります。
JavaScript版:
import { useState, useMemo } from 'react'
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: '買い物', completed: false, priority: 'high' },
{ id: 2, text: '洗濯', completed: true, priority: 'low' },
{ id: 3, text: '掃除', completed: false, priority: 'medium' }
])
const [filter, setFilter] = useState('all')
const [sortBy, setSortBy] = useState('id')
// フィルタリングとソートをメモ化
const filteredAndSortedTodos = useMemo(() => {
console.log('フィルタリングとソート実行')
// フィルタリング
let filtered = todos
if (filter === 'active') {
filtered = todos.filter(todo => !todo.completed)
} else if (filter === 'completed') {
filtered = todos.filter(todo => todo.completed)
}
// ソート
const sorted = [...filtered].sort((a, b) => {
if (sortBy === 'priority') {
const priorityOrder = { high: 3, medium: 2, low: 1 }
return priorityOrder[b.priority] - priorityOrder[a.priority]
}
return a.id - b.id
})
return sorted
}, [todos, filter, sortBy])
return (
<div>
<select value={filter} onChange={(e) => setFilter(e.target.value)}>
<option value="all">すべて</option>
<option value="active">未完了</option>
<option value="completed">完了</option>
</select>
<select value={sortBy} onChange={(e) => setSortBy(e.target.value)}>
<option value="id">ID順</option>
<option value="priority">優先度順</option>
</select>
<ul>
{filteredAndSortedTodos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
</div>
)
}
TypeScript版:
import { useState, useMemo } from 'react'
type Priority = 'high' | 'medium' | 'low'
type FilterType = 'all' | 'active' | 'completed'
type SortType = 'id' | 'priority'
interface Todo {
id: number
text: string
completed: boolean
priority: Priority
}
function TodoList(): JSX.Element {
const [todos, setTodos] = useState<Todo[]>([
{ id: 1, text: '買い物', completed: false, priority: 'high' },
{ id: 2, text: '洗濯', completed: true, priority: 'low' },
{ id: 3, text: '掃除', completed: false, priority: 'medium' }
])
const [filter, setFilter] = useState<FilterType>('all')
const [sortBy, setSortBy] = useState<SortType>('id')
const filteredAndSortedTodos = useMemo(() => {
console.log('フィルタリングとソート実行')
let filtered = todos
if (filter === 'active') {
filtered = todos.filter(todo => !todo.completed)
} else if (filter === 'completed') {
filtered = todos.filter(todo => todo.completed)
}
const sorted = [...filtered].sort((a, b) => {
if (sortBy === 'priority') {
const priorityOrder: Record<Priority, number> = { high: 3, medium: 2, low: 1 }
return priorityOrder[b.priority] - priorityOrder[a.priority]
}
return a.id - b.id
})
return sorted
}, [todos, filter, sortBy])
return (
<div>
<select value={filter} onChange={(e) => setFilter(e.target.value as FilterType)}>
<option value="all">すべて</option>
<option value="active">未完了</option>
<option value="completed">完了</option>
</select>
<select value={sortBy} onChange={(e) => setSortBy(e.target.value as SortType)}>
<option value="id">ID順</option>
<option value="priority">優先度順</option>
</select>
<ul>
{filteredAndSortedTodos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
</div>
)
}
useCallback
関数自体をメモ化して、React.memoとの組み合わせを正しく機能させる方法を習得できます。
「React.memoを使っているのになぜか再レンダリングされる」という典型的なバグの原因と解決策を理解できます。
関数をメモ化
コンポーネントが再レンダリングされるたびに、その内部で定義した関数は毎回新しいオブジェクトとして生成されます。
React.memoでラップした子コンポーネントのpropsに関数を渡すと、「関数が変わった=propsが変わった」と判定されてしまい、メモ化が効きません。
useCallbackを使うことで関数の同一性を保ち、不要な再レンダリングを防ぎます。
JavaScript版:
import { useState, useCallback, memo } from 'react'
// memoで最適化されたコンポーネント
const Button = memo(function Button({ onClick, children }) {
console.log('Buttonが再レンダリング')
return <button onClick={onClick}>{children}</button>
})
function App() {
const [count, setCount] = useState(0)
const [text, setText] = useState('')
// ❌ 悪い例:毎回新しい関数が作成される
// const handleClick = () => {
// setCount(count + 1)
// }
// ✅ 良い例:useCallbackで関数をメモ化
const handleClick = useCallback(() => {
setCount(prev => prev + 1)
}, []) // 依存配列が空なので一度だけ作成
return (
<div>
<input value={text} onChange={(e) => setText(e.target.value)} />
<p>カウント: {count}</p>
{/* textが変わってもButtonは再レンダリングされない */}
<Button onClick={handleClick}>+1</Button>
</div>
)
}
useCallBackを使わない場合、
Appが再レンダリング
↓
handleClick を再定義(新しいオブジェクトとして)
↓
子コンポーネントに渡す
↓
memo が「前回と同じprops?」と比較
↓
handleClick の参照が違う → 「変わった!」と誤検知
↓
子コンポーネントも再レンダリング(本当は不要なのに)
TypeScript版:
import { useState, useCallback, memo } from 'react'
interface ButtonProps {
onClick: () => void
children: React.ReactNode
}
const Button = memo(function Button({ onClick, children }: ButtonProps): JSX.Element {
console.log('Buttonが再レンダリング')
return <button onClick={onClick}>{children}</button>
})
function App(): JSX.Element {
const [count, setCount] = useState<number>(0)
const [text, setText] = useState<string>('')
const handleClick = useCallback(() => {
setCount(prev => prev + 1)
}, [])
return (
<div>
<input value={text} onChange={(e) => setText(e.target.value)} />
<p>カウント: {count}</p>
<Button onClick={handleClick}>+1</Button>
</div>
)
}
useCallbackと依存配列
依存配列に指定した値が変わったときだけ、関数を再生成します。
たとえばtodo.idが変わったときだけhandleDeleteを作り直すといったコントロールが可能です。
依存配列を正しく設定することで、不必要な再生成を防ぎつつ、古い値を参照するバグも回避できます。
JavaScript版:
import { useState, useCallback } from 'react'
function TodoItem({ todo, onDelete }) {
const [isHovered, setIsHovered] = useState(false)
// todoが変わったときだけ関数を再作成
const handleDelete = useCallback(() => {
onDelete(todo.id)
}, [todo.id, onDelete])
return (
<div
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<span>{todo.text}</span>
{isHovered && <button onClick={handleDelete}>削除</button>}
</div>
)
}
React.memo + useCallback + useMemo の組み合わせ
3つの最適化ツールを実際のアプリに組み合わせて適用する方法を習得できます。
「どこに何を使えばいいか」の判断基準が身につき、実務レベルのパフォーマンス設計ができるようになります。
実際のアプリでは、これら3つを組み合わせて使います。
TodoAppを例に、子コンポーネントのメモ化(React.memo)・統計計算とフィルタリングのキャッシュ(useMemo)・イベントハンドラの安定化(useCallback) を同時に適用する方法を見ていきましょう。
JavaScript版:
import { useState, useMemo, useCallback, memo } from 'react'
// 子コンポーネント
const TodoItem = memo(function TodoItem({ todo, onToggle, onDelete }) {
console.log('TodoItemが再レンダリング:', todo.id)
return (
<div>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
<span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.text}
</span>
<button onClick={() => onDelete(todo.id)}>削除</button>
</div>
)
})
// 親コンポーネント
function TodoApp() {
const [todos, setTodos] = useState([
{ id: 1, text: '買い物', completed: false },
{ id: 2, text: '洗濯', completed: false }
])
const [filter, setFilter] = useState('all')
// 重い計算をメモ化
const stats = useMemo(() => {
console.log('統計を計算')
return {
total: todos.length,
completed: todos.filter(t => t.completed).length,
active: todos.filter(t => !t.completed).length
}
}, [todos])
// フィルタリングをメモ化
const filteredTodos = useMemo(() => {
console.log('フィルタリング実行')
if (filter === 'active') return todos.filter(t => !t.completed)
if (filter === 'completed') return todos.filter(t => t.completed)
return todos
}, [todos, filter])
// 関数をメモ化
const handleToggle = useCallback((id) => {
setTodos(prev => prev.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
))
}, [])
const handleDelete = useCallback((id) => {
setTodos(prev => prev.filter(todo => todo.id !== id))
}, [])
return (
<div>
<div>
<button onClick={() => setFilter('all')}>すべて ({stats.total})</button>
<button onClick={() => setFilter('active')}>未完了 ({stats.active})</button>
<button onClick={() => setFilter('completed')}>完了 ({stats.completed})</button>
</div>
<div>
{filteredTodos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={handleToggle}
onDelete={handleDelete}
/>
))}
</div>
</div>
)
}
TypeScript版:
import { useState, useMemo, useCallback, memo } from 'react'
interface Todo {
id: number
text: string
completed: boolean
}
interface TodoItemProps {
todo: Todo
onToggle: (id: number) => void
onDelete: (id: number) => void
}
const TodoItem = memo(function TodoItem({
todo,
onToggle,
onDelete
}: TodoItemProps): JSX.Element {
console.log('TodoItemが再レンダリング:', todo.id)
return (
<div>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
<span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.text}
</span>
<button onClick={() => onDelete(todo.id)}>削除</button>
</div>
)
})
function TodoApp(): JSX.Element {
const [todos, setTodos] = useState<Todo[]>([
{ id: 1, text: '買い物', completed: false },
{ id: 2, text: '洗濯', completed: false }
])
const [filter, setFilter] = useState<'all' | 'active' | 'completed'>('all')
const stats = useMemo(() => {
console.log('統計を計算')
return {
total: todos.length,
completed: todos.filter(t => t.completed).length,
active: todos.filter(t => !t.completed).length
}
}, [todos])
const filteredTodos = useMemo(() => {
console.log('フィルタリング実行')
if (filter === 'active') return todos.filter(t => !t.completed)
if (filter === 'completed') return todos.filter(t => t.completed)
return todos
}, [todos, filter])
const handleToggle = useCallback((id: number): void => {
setTodos(prev => prev.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
))
}, [])
const handleDelete = useCallback((id: number): void => {
setTodos(prev => prev.filter(todo => todo.id !== id))
}, [])
return (
<div>
<div>
<button onClick={() => setFilter('all')}>すべて ({stats.total})</button>
<button onClick={() => setFilter('active')}>未完了 ({stats.active})</button>
<button onClick={() => setFilter('completed')}>完了 ({stats.completed})</button>
</div>
<div>
{filteredTodos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={handleToggle}
onDelete={handleDelete}
/>
))}
</div>
</div>
)
}
よくある間違い
最適化の「やりすぎ」や「設定ミス」によって起きる典型的なバグを事前に回避できます。
パフォーマンス改善のつもりがかえってコードを壊してしまう落とし穴を知っておくことで、安全に最適化を適用できるようになります。
間違い1:すべてをメモ化する
useMemoやuseCallbackはメモリを使い、比較処理のコストも発生します。
文字列の結合など軽い処理に使っても意味がなく、むしろコードが読みにくくなるだけです。
最適化は「重い処理」や「頻繁に再レンダリングされるコンポーネント」に絞って適用しましょう。
// ❌ 悪い例:過剰な最適化
function SimpleComponent({ name }) {
// シンプルな文字列結合にuseMemoは不要
const greeting = useMemo(() => `こんにちは、${name}さん`, [name])
return <div>{greeting}</div>
}
// ✅ 良い例:シンプルに書く
function SimpleComponent({ name }) {
const greeting = `こんにちは、${name}さん`
return <div>{greeting}</div>
}
間違い2:依存配列を忘れる
useMemoやuseCallbackの依存配列に使用している変数をすべて含めないと、古い値を参照し続けるバグが発生します。
ESLintのexhaustive-depsルールを有効にしておくと、こうした抜け漏れを自動で検出できます。
// ❌ 悪い例:依存配列が不完全
const filtered = useMemo(() => {
return items.filter(item => item.category === selectedCategory)
}, [items]) // selectedCategoryが含まれていない
// ✅ 良い例:すべての依存を含める
const filtered = useMemo(() => {
return items.filter(item => item.category === selectedCategory)
}, [items, selectedCategory])
間違い3:オブジェクトや配列を依存配列に
オブジェクトや配列はレンダリングのたびに新しい参照が作られるため、React.memoの比較で「変わった」と判定されます。
子コンポーネントにオブジェクト・配列をpropsとして渡す場合は、useMemoでメモ化して参照の同一性を保ちましょう。
// ❌ 悪い例:毎回新しいオブジェクトが作成される
function Parent() {
const config = { theme: 'dark' } // 毎回新しいオブジェクト
return <Child config={config} />
}
const Child = memo(function Child({ config }) {
// configは毎回変わるため、memoが効かない
return <div>{config.theme}</div>
})
// ✅ 良い例:useMemoでメモ化
function Parent() {
const config = useMemo(() => ({ theme: 'dark' }), [])
return <Child config={config} />
}
パフォーマンス測定
最適化の効果を数値で確認する方法を習得できます。
「なんとなく速くなった気がする」ではなく、計測に基づいて最適化を判断する習慣が身につきます。
React DevTools Profiler
React DevToolsに内蔵されたProfilerを使うと、各コンポーネントのレンダリング時間をビジュアルで確認できます。
コード上では<Profiler>コンポーネントを使うことで、レンダリングの実時間をログに出力することも可能です。
import { Profiler } from 'react'
function onRenderCallback(
id,
phase,
actualDuration,
baseDuration,
startTime,
commitTime
) {
console.log(`${id} の ${phase} フェーズ`)
console.log(`実際の時間: ${actualDuration}ms`)
}
function App() {
return (
<Profiler id="App" onRender={onRenderCallback}>
<TodoApp />
</Profiler>
)
}
console.timeでの測定
より手軽に特定の処理時間を計測したい場合は、console.time / console.timeEndを使います。
最適化の前後で計測し、改善効果を数値で比較するのに便利です。
function ExpensiveComponent() {
console.time('重い処理')
// 重い処理
let result = 0
for (let i = 0; i < 1000000; i++) {
result += i
}
console.timeEnd('重い処理')
return <div>{result}</div>
}
最適化のガイドライン
いつ最適化すべきで、いつすべきでないかの判断基準が身につきます。
「とりあえず全部メモ化する」「問題が出てから慌てて対処する」という両極端を避け、適切なタイミングで適切な手を打てるようになります。
いつ最適化すべきか?
- パフォーマンス問題が実際に発生している
- コンポーネントが頻繁に再レンダリングされる
- 重い計算やデータ変換がある
- 大きなリストを表示している
いつ最適化すべきでないか?
- シンプルなコンポーネント
- 再レンダリングが速い
- 計算が軽い
- まだ問題が起きていない
最適化の順序
感覚で最適化するのではなく、以下の順序を守ることで無駄な作業を防げます。
- まず計測する(React DevTools Profiler)
- ボトルネックを特定
- 必要な箇所だけ最適化
- 再度計測して効果を確認
まとめ
この記事では、Reactのパフォーマンス最適化を詳しく学びました。
重要なポイント:
- React.memo:propsが変わらなければ再レンダリングをスキップ
- useMemo:重い計算結果をメモ化
- useCallback:関数をメモ化
- 過剰な最適化は避ける
- 依存配列を正しく設定する
最適化の3つのツール:
| ツール | 対象 |
|---|---|
React.memo | コンポーネント全体をメモ化 |
useMemo | 値や計算結果をメモ化 |
useCallback | 関数をメモ化 |
ベストプラクティス:
- 実際に問題が起きてから最適化する
- パフォーマンスを計測してから最適化する
- シンプルなコンポーネントは最適化しない
- 依存配列を正確に指定する
- オブジェクトや配列のpropsはメモ化する
最適化は諸刃の剣です。
コードを複雑にするため、本当に必要な場合だけ適用しましょう!
次のステップ: 次回は、TypeScriptの高度な型定義について学びます。ジェネリクス、ユーティリティ型、型ガードなど、より型安全なReactアプリケーションを作る方法を詳しく解説します!


























