404 Not Found

404 Not Found


nginx

パフォーマンスの最適化:useMemo と useCallback

1. 学習内容



2. 検索フィルターページの物語

パフォーマンス最適化の3つの要素間の関係

100%
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)が行われるたびに、filtersortが再実行されてしまい、そのたびに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])

(1) ▶ サンプル:性能比較

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
▶ 試してみよう

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:子コンポーネントの再レンダリングを防ぐ

JSX
// ============================================
// Example:useCallback + React.memo For use in combination with
// ============================================

// ---- Child component:use  React.memo Package ----
const ExpensiveList = React.memo(function ExpensiveList({ items, onItemClick }) {
  console.log('ExpensiveList Render Again!')
  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>
          {item.name}
          <button onClick={() => onItemClick(item.id)}>Select</button>
        </li>
      ))}
    </ul>
  )
})

// ---- Parent Component ----
function App() {
  const [count, setCount] = useState(0)
  const [items] = useState([
    { id: 1, name: 'Project A' },
    { id: 2, name: 'Project B' },
    { id: 3, name: 'Project C' }
  ])

  // ❌ Every time App Rendering,All function references have changed → React.memo Failure
  const handleClickBad = (id) => console.log('Select:', id)

  // ✅ Function references are immutable → React.memo Effective
  const handleClick = useCallback((id) => {
    console.log('Select:', id)
  }, [])

  return (
    <div>
      <p>Counter:{count}</p>
      <button onClick={() => setCount(c => c + 1)}>Change Count</button>
      {/* Tap the button to change the count → No impact ExpensiveList Rendering of */}
      <ExpensiveList items={items} onItemClick={handleClick} />
    </div>
  )
}
// When clicking "Change Count" :
// ❌ Without useCallback → ExpensiveList Render again
// ✅ has  useCallback → ExpensiveList Skip rendering(Performance Improvements)


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. 完全な例:チャットアプリのメッセージ一覧

JSX
// ============================================
// Complete Example:Chat Message List(Comprehensive Performance Optimization)
// ============================================

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

// ---- News Item(React.memo + useCallback) ----
const MessageItem = memo(function MessageItem({ message, onReact, onReply }) {
  console.log(`Rendering Message:${message.id}`)
  
  return (
    <div style={{
      padding: '12px',
      margin: '8px 0',
      backgroundColor: message.isMine ? '#e6f7ff' : '#f5f5f5',
      borderRadius: '8px'
    }}>
      <div style={{ display: 'flex', justifyContent: 'space-between' }}>
        <strong>{message.author}</strong>
        <span style={{ color: '#999', fontSize: '12px' }}>
          {message.time}
        </span>
      </div>
      <p style={{ margin: '8px 0' }}>{message.text}</p>
      <div style={{ display: 'flex', gap: '8px' }}>
        <button onClick={() => onReact(message.id)} style={smallBtn}>
          👍 {message.reactions}
        </button>
        <button onClick={() => onReply(message.id)} style={smallBtn}>
          💬 Reply
        </button>
      </div>
    </div>
  )
})

// ---- Main Component ----
function ChatApp() {
  const [messages, setMessages] = useState(generateMessages(100))
  const [filter, setFilter] = useState('All')
  const [unreadCount, setUnreadCount] = useState(5)

  // useMemo:Filter Messages(100 Filter by row)
  const filteredMessages = useMemo(
    () => filter === 'All'
      ? messages
      : messages.filter(m => filter === 'My' ? m.isMine : !m.isMine),
    [messages, filter]
  )

  // useCallback:Keep References Consistent,let  MessageItem 's  memo Effective
  const handleReact = useCallback((id) => {
    setMessages(prev => prev.map(m =>
      m.id === id ? { ...m, reactions: m.reactions + 1 } : m
    ))
  }, [])

  const handleReply = useCallback((id) => {
    console.log('Reply to a message:', id)
  }, [])

  // Calculate Statistical Information
  const stats = useMemo(() => ({
    total: messages.length,
    mine: messages.filter(m => m.isMine).length,
    totalReactions: messages.reduce((s, m) => s + m.reactions, 0)
  }), [messages])

  return (
    <div style={{ maxWidth: '600px', margin: '0 auto' }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
        <h2>💬 Chat({unreadCount} Unread posts)</h2>
        <button onClick={() => setUnreadCount(c => c + 1)}>
          Increase the unread count(Test Performance)
        </button>
      </div>

      {/* Statistics(useMemo) */}
      <div style={{ display: 'flex', gap: '16px', marginBottom: '12px', fontSize: '13px', color: '#666' }}>
        <span>Total {stats.total} items</span>
        <span>My {stats.mine} items</span>
        <span>👍 {stats.totalReactions}</span>
      </div>

      {/* Filter Button */}
      <div style={{ marginBottom: '12px' }}>
        {['All', 'My', 'Other people's'].map(f => (
          <button
            key={f}
            onClick={() => setFilter(f)}
            style={{
              padding: '4px 12px', margin: '0 4px',
              backgroundColor: filter === f ? '#1890ff' : '#f0f0f0',
              color: filter === f ? 'white' : '#333',
              border: 'none', borderRadius: '4px', cursor: 'pointer'
            }}
          >
            {f}
          </button>
        ))}
      </div>

      {/* News List */}
      <div style={{ maxHeight: '500px', overflow: 'auto' }}>
        {filteredMessages.map(msg => (
          <MessageItem
            key={msg.id}
            message={msg}
            onReact={handleReact}   // ✅ Stable citation
            onReply={handleReply}   // ✅ Stable citation
          />
        ))}
      </div>
    </div>
  )
}

function generateMessages(count) {
  const names = ['Alice', 'Bob', 'Charlie', 'Diana']
  return Array.from({ length: count }, (_, i) => ({
    id: i + 1,
    author: names[i % 4],
    text: `This is the ${i + 1} The content of the message`,
    time: `${String(10 + i % 8).padStart(2, '0')}:${String(i % 60).padStart(2, '0')}`,
    isMine: i % 3 === 0,
    reactions: Math.floor(Math.random() * 5)
  }))
}

const smallBtn = {
  padding: '2px 8px', fontSize: '12px',
  border: '1px solid #d9d9d9', borderRadius: '4px',
  backgroundColor: 'white', cursor: 'pointer'
}

期待される出力:100件のメッセージを含むチャットリスト。「未読数を増やす」をクリックしても、useCallback および React.memo の影響により、メッセージリストは再描画されず(コンソールに出力は表示されない)、未読数のみが変更されます。


(1) ▶ サンプル:useCallback により、useEffect の複数回実行が防止される

JSX
// ============================================
// Example:useCallback In conjunction with useEffect Avoiding Infinite Loops
// Features:Display useCallback in  useEffect The Key Role of Dependencies
// ============================================

import { useState, useEffect, useCallback } from 'react'

function SearchComponent() {
  const [query, setQuery] = useState('')
  const [results, setResults] = useState([])
  const [page, setPage] = useState(1)

  // ❌ Question:Each render is a new function,As useEffect Dependencies can cause an infinite loop
  // const fetchResults = async () => {
  //   const res = await fetch(`/api/search?q=${query}&page=${page}`)
  //   const data = await res.json()
  //   setResults(data)
  // }

  // ✅ Resolve:useCallback Stable Function References
  const fetchResults = useCallback(async () => {
    // Simulation API Request
    const mockData = [
      { id: 1, title: `Results ${query} - Page ${page}` },
      { id: 2, title: `Related Articles About ${query}` },
      { id: 3, title: `${query} Best Practices` }
    ]
    setResults(mockData)
  }, [query, page])  // query or  page Create a new function only when there is a change

  // Dependency fetchResults(Stable Citation,It won't go into an infinite loop)
  useEffect(() => {
    if (query.trim()) {
      fetchResults()
    }
  }, [fetchResults])  // fetchResults Citation unchanged → useEffect Will not be executed again

  return (
    <div style={{ maxWidth: '500px', margin: '0 auto' }}>
      <h2>🔍 Search(useCallback Demo)</h2>
      <input
        value={query}
        onChange={e => setQuery(e.target.value)}
        placeholder="Enter a search term..."
        style={{ width: '100%', padding: '8px', border: '1px solid #d9d9d9', borderRadius: '4px' }}
      />
      <div style={{ marginTop: '8px', display: 'flex', gap: '8px', alignItems: 'center' }}>
        <button onClick={() => setPage(p => Math.max(1, p - 1))} disabled={page <= 1}>Previous Page</button>
        <span>Page  {page} </span>
        <button onClick={() => setPage(p => p + 1)}>Next Page</button>
      </div>
      <ul style={{ marginTop: '12px', listStyle: 'none', padding: 0 }}>
        {results.map(r => (
          <li key={r.id} style={{ padding: '8px', borderBottom: '1px solid #f0f0f0' }}>
            {r.title}
          </li>
        ))}
      </ul>
      {!query && <p style={{ color: '#999', textAlign: 'center' }}>Enter a keyword to start your search</p>}
    </div>
  )
}
▶ 試してみよう

(2) ▶ サンプル: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>
  )
}
▶ 試してみよう

(3) ▶ サンプル:React.memo と useCallback を使ったリストの最適化

JSX
const ListItem = React.memo(function ListItem({ name, price, onRemove }) {
  console.log(`Rendering: ${name}`)
  return (
    <div style={{ display: 'flex', justifyContent: 'space-between', padding: 8, borderBottom: '1px solid #f0f0f0' }}>
      <span>{name} - ${price}</span>
      <button onClick={() => onRemove(name)} style={{ color: '#ff4d4f', border: 'none', cursor: 'pointer' }}>Remove</button>
    </div>
  )
})

function ProductList() {
  const [products, setProducts] = useState([
    { name: 'Keyboard', price: 79 },
    { name: 'Mouse', price: 49 },
    { name: 'Monitor', price: 399 },
    { name: 'Headphones', price: 129 },
  ])
  const [filter, setFilter] = useState('')

  const handleRemove = useCallback((name) => {
    setProducts(prev => prev.filter(p => p.name !== name))
  }, [])

  const filtered = useMemo(() => products.filter(p => p.name.toLowerCase().includes(filter.toLowerCase())), [products, filter])

  return (
    <div style={{ maxWidth: 400, margin: '0 auto' }}>
      <input value={filter} onChange={e => setFilter(e.target.value)} placeholder="Filter..." style={{ width: '100%', padding: 8, marginBottom: 8, borderRadius: 4 }} />
      {filtered.map(p => <ListItem key={p.name} name={p.name} price={p.price} onRemove={handleRemove} />)}
    </div>
  )
}
▶ 試してみよう

(4) ▶ サンプル: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>
  )
}
▶ 試してみよう

❓ よくある質問

Q useMemouseCallback は常にパフォーマンスを向上させるのでしょうか?
A 必ずしもそうとは限りません。これらには独自のオーバーヘッドがあり、レンダリングのたびに dependencies 配列と比較処理が行われます。使いすぎると、かえって処理が遅くなる可能性があります。Reactの公式な推奨事項:まずは最適化されていないバージョンを記述し、遅延に気づいた場合にのみ最適化を行うこと。経験則として、React.memo子コンポーネントに渡される計算、またはオーバーヘッドが1 msを超える計算にのみこれらを使用してください。
Q useMemoと通常の変数の違いは何ですか?
A useMemoには「記憶」機能があります。依存関係に変更がない場合、式を再評価することなく、キャッシュされた値を直接使用します。通常の変数は、レンダリングのたびに再評価されます。しかし、useMemoはレンダリング中に依存関係の配列を比較する必要があるため、「単純な計算では、実際にはuseMemoの方が処理が遅くなります。」
Q 関数の参照を保持するために、いつ useCallback を使用すべきですか?
A 以下の2つの条件の両方が満たされる場合にのみ使用する必要があります:① その関数が React.memo の子コンポーネントにプロパティとして渡されていること;② その関数が useEffect フックの dependencies 配列に含まれていること。これらの条件のいずれも当てはまらない場合は、関数を直接記述するだけでよく、useCallback は不要です。
Q useMemouseCallback の違いは何ですか?
A useMemo(() => value, deps) は計算結果(値)をキャッシュするのに対し、useCallback(() => fn, deps) は関数参照をキャッシュします。実際、useCallback(fn, deps)useMemo(() => fn, deps) と同等です。子コンポーネントに渡される関数をキャッシュする必要がある場合は useCallback を、計算結果をキャッシュする必要がある場合は useMemo を使用してください。

📖 まとめ


📝 練習問題

  1. 基本問題(難易度 ⭐):整数 n を入力として受け取り、n 番目の素数を計算する PrimeCalculator コンポーネントを作成してください。新しい値が入力されるたびに再計算されないよう、useMemo を使用して結果をキャッシュしてください。
  2. 上級演習(難易度 ⭐⭐):500件の商品レコードを含み、カテゴリによるフィルタリングと価格によるソートをサポートする ProductTable コンポーネントを作成してください。useMemo を使用してフィルタリングおよびソートのロジックを最適化し、useCallbackReact.memo を組み合わせてリストのレンダリングを最適化してください。
  3. 課題(難易度:⭐⭐⭐):3つの異なるディメンション(時間、地域、カテゴリ)にわたる10,000件の注文レコードを集計するDataDashboardコンポーネントを作成し、useMemoを使用して階層型キャッシュを実装してください(ディメンションが変更された際に、関連する階層のみが再計算されるようにするため)。
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%