React: 性能优化:useMemo 与 useCallback

最后更新:2026-08-26

1. 你将学到



2. 一个搜索过滤页面的故事

性能优化三件套的关系

100%
flowchart TD
    A[父组件渲染] --> B{Props 变了?}
    B -->|是| C[React.memo 拦截?]
    B -->|否| D[跳过子组件 ✅]
    C -->|浅比较相等| D
    C -->|浅比较不等| E{函数/对象是新的?}
    E -->|是| F[useCallback/useMemo]
    F --> G[稳定引用 → memo 生效 ✅]
    E -->|否| D
    
    style F fill:#e8f5e9,stroke:#2e7d32
    style D fill:#c8e6c9,stroke:#2e7d32
    style G fill:#c8e6c9,stroke:#2e7d32

(1) 痛点:每次输入都重新计算

Alice 做了一个商品搜索页面,有 10,000 件商品需要过滤:

JSX
function ShopPage() {
  const [products] = useState(generate10000Products())
  const [search, setSearch] = useState('')
  const [sortBy, setSortBy] = useState('name')

  // ❌ 问题:每次输入都重新过滤 10,000 件商品
  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">按名称</option>
        <option value="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:只在 search 或 sortBy 变化时才重新计算
  const filtered = useMemo(() => {
    console.log('重新过滤...(只在搜索词或排序变化时)')
    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">按名称</option>
        <option value="price">按价格</option>
      </select>
      <ProductList products={filtered} />
    </div>
  )
}
▶ 试一试

收益:每次输入只在 searchsortBy 变化时才重新过滤,其他状态变化(比如父组件的重渲染)不会触发过滤,性能提升 10-100 倍



3. useMemo:记住计算结果

Hook 记住什么 返回值 依赖变化时
useMemo 计算结果 缓存的值 重新计算
useCallback 函数引用 缓存的函数 创建新函数
React.memo 整个组件渲染结果 跳过重渲染 props 变化时重新渲染

(1) 基本语法

JSX
const memoizedValue = useMemo(() => {
  return 昂贵的计算()
}, [依赖1, 依赖2])
▶ 试一试
部分 说明
() => 计算 需要缓存的函数,返回计算结果
[依赖] 依赖变化时重新计算
memoizedValue 缓存的结果(依赖没变时直接返回上次结果)

(2) 适用场景

JSX
// 场景 1:数据转换(过滤、排序、分组)
const activeUsers = useMemo(
  () => users.filter(u => u.isActive).sort((a, b) => a.name.localeCompare(b.name)),
  [users]
)

// 场景 2:格式化(数字、日期、字符串拼接)
const formattedPrice = useMemo(
  () => new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY' }).format(price),
  [price]
)

// 场景 3:派生状态(从已有数据计算新数据)
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])
▶ 试一试

▶ 示例:性能对比

JSX
// ============================================
// 示例:有 useMemo vs 无 useMemo 的性能对比
// ============================================

function ExpensiveCalc({ num, unrelated }) {
  // ❌ 没有 useMemo:每次渲染都执行
  const slowResult1 = slowFibonacci(num)

  // ✅ 有 useMemo:num 没变就不重新计算
  const slowResult2 = useMemo(() => slowFibonacci(num), [num])

  return (
    <div>
      <p>num={num} 的斐波那契数列</p>
      <p>无 useMemo:{slowResult1}(每次渲染计算)</p>
      <p>有 useMemo:{slowResult2}(只在 num 变化时计算)</p>
      <p>不相关状态:{unrelated}(点按钮改变这个值)</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 计算!n=' + n)
  return b
}
// 点击改变 unrelated → 无 useMemo 的重新计算,有 useMemo 的跳过
▶ 试一试

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)

  // ❌ 每次 Parent 渲染,handleClick 都是新的函数
  // 导致 Child 的 React.memo 失效
  const handleClick = () => {
    console.log('点击了')
  }

  // ✅ useCallback:handleClick 引用不变
  // Child 的 React.memo 能正常工作(跳过重渲染)
  const handleClick = useCallback(() => {
    console.log('点击了')
  }, [])  // 空依赖:这个函数永远不会变
}
▶ 试一试

(3) React.memo:阻止子组件重渲染

JSX
// ============================================
// 示例:useCallback + React.memo 配合使用
// ============================================

// ---- 子组件:用 React.memo 包裹 ----
const ExpensiveList = React.memo(function ExpensiveList({ items, onItemClick }) {
  console.log('ExpensiveList 重新渲染!')
  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>
          {item.name}
          <button onClick={() => onItemClick(item.id)}>选中</button>
        </li>
      ))}
    </ul>
  )
})

// ---- 父组件 ----
function App() {
  const [count, setCount] = useState(0)
  const [items] = useState([
    { id: 1, name: '项目 A' },
    { id: 2, name: '项目 B' },
    { id: 3, name: '项目 C' }
  ])

  // ❌ 每次 App 渲染,函数引用都变 → React.memo 失效
  const handleClickBad = (id) => console.log('选中:', id)

  // ✅ 函数引用不变 → React.memo 生效
  const handleClick = useCallback((id) => {
    console.log('选中:', id)
  }, [])

  return (
    <div>
      <p>计数器:{count}</p>
      <button onClick={() => setCount(c => c + 1)}>改变计数</button>
      {/* 点按钮改变计数 → 不影响 ExpensiveList 的渲染 */}
      <ExpensiveList items={items} onItemClick={handleClick} />
    </div>
  )
}
// 点"改变计数"时:
// ❌ 无 useCallback → ExpensiveList 跟着重新渲染
// ✅ 有 useCallback → ExpensiveList 跳过渲染(性能提升)
▶ 试一试

5. 什么时候用,什么时候别用

(1) 应该用的情况

场景 Hook 原因
大数据过滤/排序 useMemo 避免每次渲染都遍历大量数据
复杂计算(数学/格式化) useMemo 计算结果不变就不需要重算
Props 传给 React.memo 子组件 useCallback 保持函数引用稳定,让 React.memo 生效
useEffect 依赖中的函数 useCallback 避免 useEffect 因函数引用变化而重复执行

(2) 不需要用的情况

JSX
// ❌ 不需要:简单计算
const doubled = useMemo(() => count * 2, [count])
// ✅ 直接写:count * 2 本身就已经很快了
const doubled = count * 2

// ❌ 不需要:事件处理但不传给子组件
const handleClick = useCallback(() => {
  console.log('点击')
}, [])
// ✅ 直接写即可
const handleClick = () => console.log('点击')

// ❌ 不需要:子组件没有用 React.memo
// useCallback 包装了但没人消费这个"稳定引用"
▶ 试一试

(3) 滥用的代价

JSX
// useMemo 和 useCallback 本身有开销(比较依赖数组)
// 滥用 = 引入额外的比较逻辑,比不优化更慢

// 经验法则:
// 1. 先写不加优化的版本
// 2. 感觉到卡顿(帧率 < 60fps)再用
// 3. 用 React DevTools Profiler 测量
▶ 试一试

6. 完整示例:聊天应用消息列表

JSX
// ============================================
// 完整示例:聊天消息列表(综合性能优化)
// ============================================

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

// ---- 消息项(React.memo + useCallback) ----
const MessageItem = memo(function MessageItem({ message, onReact, onReply }) {
  console.log(`渲染消息:${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}>
          💬 回复
        </button>
      </div>
    </div>
  )
})

// ---- 主组件 ----
function ChatApp() {
  const [messages, setMessages] = useState(generateMessages(100))
  const [filter, setFilter] = useState('全部')
  const [unreadCount, setUnreadCount] = useState(5)

  // useMemo:过滤消息(100 条中过滤)
  const filteredMessages = useMemo(
    () => filter === '全部'
      ? messages
      : messages.filter(m => filter === '我的' ? m.isMine : !m.isMine),
    [messages, filter]
  )

  // useCallback:保持引用稳定,让 MessageItem 的 memo 生效
  const handleReact = useCallback((id) => {
    setMessages(prev => prev.map(m =>
      m.id === id ? { ...m, reactions: m.reactions + 1 } : m
    ))
  }, [])

  const handleReply = useCallback((id) => {
    console.log('回复消息:', id)
  }, [])

  // 计算统计信息
  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>💬 聊天({unreadCount} 条未读)</h2>
        <button onClick={() => setUnreadCount(c => c + 1)}>
          增加未读数(测试性能)
        </button>
      </div>

      {/* 统计(useMemo) */}
      <div style={{ display: 'flex', gap: '16px', marginBottom: '12px', fontSize: '13px', color: '#666' }}>
        <span>共 {stats.total} 条</span>
        <span>我的 {stats.mine} 条</span>
        <span>👍 {stats.totalReactions}</span>
      </div>

      {/* 筛选按钮 */}
      <div style={{ marginBottom: '12px' }}>
        {['全部', '我的', '他人的'].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>

      {/* 消息列表 */}
      <div style={{ maxHeight: '500px', overflow: 'auto' }}>
        {filteredMessages.map(msg => (
          <MessageItem
            key={msg.id}
            message={msg}
            onReact={handleReact}   // ✅ 引用稳定
            onReply={handleReply}   // ✅ 引用稳定
          />
        ))}
      </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: `这是第 ${i + 1} 条消息的内容`,
    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,消息列表不会重新渲染(控制台无输出),只有统计数字变化。


▶ 示例 2:useCallback 防止 useEffect 重复执行

JSX 📖 仅展示
// ============================================
// 示例:useCallback 配合 useEffect 避免死循环
// 功能:展示 useCallback 在 useEffect 依赖中的关键作用
// ============================================

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

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

  // ❌ 问题:每次渲染都是新函数,作为 useEffect 依赖会导致死循环
  // const fetchResults = async () => {
  //   const res = await fetch(`/api/search?q=${query}&page=${page}`)
  //   const data = await res.json()
  //   setResults(data)
  // }

  // ✅ 解决:useCallback 稳定函数引用
  const fetchResults = useCallback(async () => {
    // 模拟 API 请求
    const mockData = [
      { id: 1, title: `结果 ${query} - 第${page}页` },
      { id: 2, title: `相关文章关于 ${query}` },
      { id: 3, title: `${query} 的最佳实践` }
    ]
    setResults(mockData)
  }, [query, page])  // query 或 page 变化时才创建新函数

  // 依赖 fetchResults(稳定引用,不会死循环)
  useEffect(() => {
    if (query.trim()) {
      fetchResults()
    }
  }, [fetchResults])  // fetchResults 引用不变 → useEffect 不会重复执行

  return (
    <div style={{ maxWidth: '500px', margin: '0 auto' }}>
      <h2>🔍 搜索(useCallback 演示)</h2>
      <input
        value={query}
        onChange={e => setQuery(e.target.value)}
        placeholder="输入搜索关键词..."
        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}>上一页</button>
        <span>第 {page} 页</span>
        <button onClick={() => setPage(p => p + 1)}>下一页</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' }}>输入关键词开始搜索</p>}
    </div>
  )
}
逻辑代码 43 行(超过 40 行限制,仅展示)

▶ 示例 3: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>
  )
}
▶ 试一试

▶ 示例 4: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>
  )
}
▶ 试一试

▶ 示例 5:何时不用 useMemo——反模式对比

JSX
function BadExample({ count }) {
  // ❌ 简单计算不需要 useMemo——创建 memo 开销 > 计算本身
  const doubled = useMemo(() => count * 2, [count])
  // ✅ 直接计算
  const doubledOk = count * 2

  // ❌ 每次渲染创建新对象作为 useMemo 依赖
  const config = useMemo(() => ({ count }), [count])
  const badResult = useMemo(() => config.count * 3, [config]) // config 每次是新引用!
  // ✅ 直接用基本类型作为依赖
  const goodResult = useMemo(() => count * 3, [count])

  return <p>Doubled: {doubledOk}, Tripled: {goodResult}</p>
}

function GoodExample({ items, query }) {
  // ✅ 过滤/排序大量数据用 useMemo
  const filtered = useMemo(() => {
    return items.filter(item =>
      item.name.toLowerCase().includes(query.toLowerCase())
    ).sort((a, b) => a.name.localeCompare(b.name))
  }, [items, query])

  // ✅ 传递给子组件的复杂对象用 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 useMemo 和 useCallback 一定能提升性能吗?
A 不一定。它们本身也有开销——每次渲染都要比较依赖数组。滥用反而变慢。React 官方建议:先写不加优化的版本,感觉到卡顿再优化。Rule of thumb:只有传给 React.memo 子组件或开销 > 1ms 的计算才需要用。
Q useMemo 和直接变量有什么区别?
A useMemo 有"记忆"——依赖没变就直接用缓存值,不重新执行计算。直接变量每次渲染都重新计算。但 useMemo 需要在渲染时比较依赖数组,所以"简单的计算用 useMemo 反而更慢"。
Q 什么时候用 useCallback 保函数引用?
A 只有当同时满足两个条件时才需要:① 函数作为 Props 传给 React.memo 子组件;② 函数在 useEffect 的依赖数组中。不满足这两个条件时,直接写函数即可,useCallback 是多余的。
Q useMemo 和 useCallback 有什么区别?
A useMemo(() => value, deps) 缓存计算结果(值),useCallback(() => fn, deps) 缓存函数引用。实际上 useCallback(fn, deps) 等价于 useMemo(() => fn, deps)。当你需要缓存一个函数传给子组件时用 useCallback;当你需要缓存一个计算结果时用 useMemo。

📖 小节


📝 作业

  1. 基础题(难度⭐):创建一个 PrimeCalculator 组件,输入数字 n,计算第 n 个质数。用 useMemo 缓存结果,避免每次输入都重新计算。
  2. 进阶题(难度⭐⭐):创建一个 ProductTable 组件,有 500 条商品数据,支持按分类筛选和按价格排序。用 useMemo 优化过滤/排序逻辑,用 useCallback + React.memo 优化列表渲染。
  3. 挑战题(难度⭐⭐⭐):创建一个 DataDashboard 组件,从 3 个不同维度(时间、地区、类别)聚合 10,000 条订单数据,用 useMemo 实现分层缓存(维度变化时只重新计算相关层)。
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏