useMemo / useCallback:什么时候用、什么时候是 over-engineering

React 新手最常踩的"过度优化"就是给所有函数 / 计算包 useMemo useCallback
官方文档 2024 之后已经明确:默认不要用,先 profile

但是有几个场景必须用。下面分清楚。

默认不要用的理由

useMemo 本身要执行:

  1. 调用 useMemo 创建 hook entry
  2. 比较依赖数组(浅比较)
  3. 决定是否复用

这些开销 > 大多数同步函数的执行成本。所以对一个"加两个数字"包 useMemo,
反而更慢。

真正需要 useMemo 的场景

场景 1:计算确实贵(毫秒级以上)

function ChartView({ rawData }: { rawData: Row[] }) {
  // 大量数据点排序 + 重计算
  const processed = useMemo(() => {
    return rawData
      .map(row => ({ ...row, score: complexCalc(row) }))
      .sort((a, b) => b.score - a.score)
      .slice(0, 100)
  }, [rawData])

  return <Chart data={processed} />
}

判断标准:profile 看到这块 > 16ms(一帧)就有必要。

场景 2:作为 useEffect / useMemo / useCallback 的依赖

function Form({ schema }: { schema: Schema }) {
  // 不 memo 的话每次渲染 validator 都是新对象,下面 useEffect 每次都跑
  const validator = useMemo(() => createValidator(schema), [schema])

  useEffect(() => {
    validator.subscribe(handleValid)
    return () => validator.unsubscribe(handleValid)
  }, [validator])
}

这是 useMemo 最常见的合理用途——稳定依赖。

场景 3:传给 memoized 子组件

const SearchResults = React.memo(function SearchResults({ items }: ...) {
  // ...
})

function Page() {
  const [query, setQuery] = useState('')
  // items 每次都是新数组 → React.memo 浅比较失败 → 子组件每次都重渲染
  // 加 useMemo 之后只有 query 变了才重新生成 items
  const items = useMemo(() => filterItems(allItems, query), [query, allItems])
  return <SearchResults items={items} />
}

如果子组件没 React.memo 包裹,这个 useMemo 完全无效——白干。

useCallback 类似

function Parent() {
  const [count, setCount] = useState(0)

  // ❌ 没必要:onClick 每次都是新引用但 Button 没 memo
  const onClick = useCallback(() => setCount(c => c + 1), [])

  return <Button onClick={onClick} />
}

useCallback 仅在以下两种情况有意义:

  1. 函数作为 useEffect / useMemo 的依赖
  2. 函数传给 React.memo 的子组件

反模式集合

❌ 给基础类型包 useMemo

const sum = useMemo(() => a + b, [a, b])
// 直接:
const sum = a + b   // 一样快

❌ 包内联对象 / 数组(除非传给 memo 子)

const style = useMemo(() => ({ color: 'red' }), [])
// 直接:
const style = { color: 'red' }   // 多创建一个对象,但 GC 几乎免费

❌ "为了未来扩展" 提前 useMemo

写代码时不要"也许将来子组件会 React.memo 所以现在先包"。等真有需要再包。

测量工具

React DevTools → Profiler → Record。看哪个组件渲染 > 5ms 或者
"unnecessarily" 标红。

Chrome DevTools → Performance → Record。看主线程任务,> 50ms 就是
long task,需要优化。

React 19 的 React Compiler

React 19+ 带的 React Compiler 会在编译期自动决定哪些值需要 memo,
届时手写 useMemo / useCallback 几乎可以全删

提前为这个未来准备:

  1. 不要为了 memo 而引入"复杂的依赖追踪"。代码清晰更重要,性能用编译器
  2. 现有的 useMemo / useCallback 可以保留,编译器会忽略它们

一个完整反例

// 全是反优化
function UserCard({ user }: { user: User }) {
  const name = useMemo(() => user.name, [user])              // ❌
  const onClick = useCallback(() => alert(user.id), [user])  // ❌(没 memo 子)
  const style = useMemo(() => ({ padding: 12 }), [])         // ❌
  return (
    <div style={style} onClick={onClick}>
      <strong>{name}</strong>
    </div>
  )
}

清版:

function UserCard({ user }: { user: User }) {
  return (
    <div style={{ padding: 12 }} onClick={() => alert(user.id)}>
      <strong>{user.name}</strong>
    </div>
  )
}

后者更短、更快、更易读。

踩过的坑

  • 给 useMemo 的依赖数组写错(漏依赖 / 多依赖)是 React 最常见 bug,
    eslint-plugin-react-hooksexhaustive-deps 规则帮你查。
  • 复杂表单的 onChange 用 useCallback 包并传给 memoized Input 子组件——
    看起来对,但 input 上的 onChange 引用是否稳定通常不是性能瓶颈。
    先 profile 再优化。
  • useState 的 setter 写进 useCallback 的依赖数组:setter 永远稳定,
    写了不影响但说明你没理解 setter 的行为。可以省略。
精确评价 共 0 人评价
可复现性
可复现 · 0 不可复现 · 0
文风
文风流畅 · 0 文风晦涩 · 0
立场
支持 · 0 反对 · 0

登录后即可对本帖作出评价。

评论区 0 条 · 所有人可在此交流

登录后参与评论。

还没有评论,来说两句。