最后更新:2026-01-10

useCallbackuseMemo 是两个很容易被滥用的 Hook。

我见过不少项目代码,几乎每个函数定义都套了 useCallback,每个计算都套了 useMemo,理由是”优化性能”。结果代码变复杂了,性能也没什么实际改善,反而因为依赖项没填对造成了 bug。

说说我自己是怎么理解这两个的。

它们的作用是”缓存”,不是”优化”

先从根上搞清楚:React 函数组件每次 render 都会重新执行,函数体里所有的变量和函数定义都会重新创建。

1
2
3
4
5
6
function App() {
// 每次 render,handleClick 都是一个全新的函数,引用不同
const handleClick = () => {
console.log('clicked')
}
}

useCallback 的作用是在依赖项不变的情况下,返回同一个函数引用:

1
2
3
const handleClick = useCallback(() => {
console.log('clicked')
}, []) // 依赖项为空,handleClick 引用永远不变

useMemo 类似,缓存的是值(计算结果):

1
2
3
const result = useMemo(() => {
return expensiveCalculation(a, b)
}, [a, b]) // 只有 a 或 b 变了才重新计算

但这里有个关键问题:缓存本身也有开销。React 需要存储之前的值、比较依赖项。如果你缓存的东西不值这个开销,用了反而变慢。

什么情况下用 useCallback 才有意义

场景一:子组件使用了 React.memo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 子组件用 memo 包裹,只有 props 变了才重新渲染
const Button = React.memo(({ onClick, label }) => {
console.log('Button render')
return <button onClick={onClick}>{label}</button>
})

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

// 没有 useCallback:每次 Parent render,onClick 都是新引用
// Button 的 props 变了 → Button 重新渲染,memo 白用了
const handleClick = () => {
console.log('clicked')
}

// 有 useCallback:引用稳定,Button 不会因为 Parent render 而重新渲染
const handleClick = useCallback(() => {
console.log('clicked')
}, [])

return (
<>
<p>{count}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
<Button onClick={handleClick} label="操作" />
</>
)
}

这才是 useCallback 真正有价值的场景:配合 React.memo 避免不必要的子组件渲染。

如果子组件没有用 memo,父组件每次 render 它都会重新渲染,useCallback 没有任何意义。

场景二:作为其他 Hook 的依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function useAutoFetch(fetchFn) {
useEffect(() => {
fetchFn()
}, [fetchFn]) // fetchFn 作为依赖
}

function App() {
// 如果 fetchData 没有 useCallback,每次 render 都是新引用
// useEffect 的依赖变了 → 每次 render 都触发请求
const fetchData = useCallback(async () => {
const data = await api.getData()
setData(data)
}, []) // 稳定引用

useAutoFetch(fetchData)
}

这种情况函数作为 useEffectuseCallback 等的依赖项时,需要保证引用稳定。

什么情况下用 useMemo 才有意义

场景一:开销大的计算

1
2
3
4
5
6
7
8
9
10
function DataTable({ rawData, filters }) {
// 数据量大时,过滤 + 排序可能比较耗时
const processedData = useMemo(() => {
return rawData
.filter(item => matchesFilters(item, filters))
.sort((a, b) => a.date - b.date)
}, [rawData, filters])

return <Table data={processedData} />
}

这里如果 rawDatafilters 没变,就不需要重新计算,用 useMemo 合理。

但如果数据量很小(几十条),过滤排序本来就很快,用 useMemo 反而多了依赖比较的开销。

场景二:稳定对象引用,配合 memo

1
2
3
4
5
6
7
8
9
10
11
12
function Child({ config }) { ... }
const MemoChild = React.memo(Child)

function Parent({ theme }) {
// 每次 render config 都是新对象,MemoChild 会重新渲染
const config = { theme, size: 'large' }

// useMemo 保证对象引用稳定
const config = useMemo(() => ({ theme, size: 'large' }), [theme])

return <MemoChild config={config} />
}

原理和 useCallback 类似,只是缓存的是对象而不是函数。

最常见的错误用法

错误一:没有 memo 的子组件加 useCallback

1
2
3
4
5
6
7
8
9
10
11
12
// Child 没有用 React.memo
function Child({ onClick }) {
return <button onClick={onClick}>click</button>
}

function Parent() {
// 这个 useCallback 没有任何作用
// Child 每次都会随 Parent 重新渲染
const handleClick = useCallback(() => {}, [])

return <Child onClick={handleClick} />
}

useCallback 保证了引用稳定,但子组件没有 memo,根本不检查 props 是否变化,照样重新渲染。白写了。

错误二:依赖项写错

1
2
3
4
5
6
const [keyword, setKeyword] = useState('')

// 依赖项漏了 keyword,闭包里的 keyword 永远是初始值
const search = useCallback(() => {
api.search(keyword) // 总是空字符串
}, []) // ❌ 应该是 [keyword]

这类 bug 很隐蔽,结果表现是”搜索功能不生效”,排查半天找不到原因。

错误三:useMemo 处理简单计算

1
2
3
4
5
// 完全没必要,a + b 本来就是纳秒级的
const sum = useMemo(() => a + b, [a, b])

// 直接写就行
const sum = a + b

我现在的使用原则

  • 默认不用,遇到性能问题再考虑
  • useCallback 前先问:子组件有没有用 React.memo?这个函数有没有作为其他 Hook 的依赖?
  • useMemo 前先问:这个计算真的耗时吗?能不能直接算?
  • 依赖项一定要填完整,用 eslint-plugin-react-hooks 帮你检查

说实话,大多数应用的性能瓶颈不在组件重新渲染上,而在网络请求、大列表渲染等地方。盲目用这两个 Hook,代码变复杂,bug 变多,收益却微乎其微。用之前先 profiling,找到真正的瓶颈再优化。