useMemo 你就把它当成:“缓存一个计算结果/缓存一个引用”,只有依赖变了才重新算。它解决两类最常见问题:性能引用变化导致的重复触发

1) useMemo 到底做了什么?

const value = useMemo(() => computeExpensive(x), [x])

  • React 先算一次 computeExpensive(x),把结果 记住

  • 之后组件每次重新渲染:

    • 如果 x 没变:直接用旧结果,不重新算

    • 如果 x 变了:重新算一次并更新缓存

一句话:依赖不变 → 复用旧结果;依赖变化 → 重新计算。

2) 用途 A:优化“很慢的计算”

const doubled = useMemo(() => slowDouble(number), [number])

效果:

  • 改 number → 需要重新算(合理)

  • 切换主题 dark → 不用重新算(省掉卡顿)

✅ 什么时候值得用?

  • 计算真的慢(大循环、复杂过滤排序、图表数据处理、加密/哈希、复杂格式化)

  • 或者计算量随数据规模变大很明显(比如 1w 条列表的 filter + sort)

3) 用途 B:解决“引用每次变导致 useEffect 乱触发”

JS 里对象/数组比较的是 引用地址,不是内容。

问题例子

const themeStyle = { color: dark ? "white" : "black" }

useEffect(() => {
  console.log("theme changed")
}, [themeStyle])

每次渲染都会创建一个新对象 {...},引用永远不一样 → effect 每次都触发。

用 useMemo 修

const themeStyle = useMemo(
  () => ({ color: dark ? "white" : "black" }),
  [dark]
)

这样只有 dark 变,themeStyle 的引用才变,effect 才会触发。

你没理解的点其实就一个:useEffect 的依赖比较方式

React 判断依赖变没变,不是看“内容一样不一样”,而是看是不是同一个引用(内存地址)

4) 没有 useMemo:为什么会“乱触发”?

const config = { chainId, address }
useEffect(() => {
  console.log("run")
}, [config])

关键:{ chainId, address } 这一行 每次渲染都会创建一个全新的对象

即使 chainId 和 address 都没变,config 也是“新对象”。

你可以想象成这样:

  • 第1次渲染:config 指向 0xAAA

  • 第2次渲染:又 new 了一个对象,config 指向 0xBBB

  • 第3次渲染:又 new 了一个对象,config 指向 0xCCC

React 看依赖 [config]:

“咦,引用从 0xAAA 变成 0xBBB 了 → 依赖变了 → effect 执行。”

所以你会看到:只要组件重新渲染一次(哪怕因为 theme、输入框、别的 state),effect 就会跑一次

5) 有 useMemo:为什么就不乱触发?

const config = useMemo(() => ({ chainId, address }), [chainId, address])

useEffect(() => {
  console.log("run only when chainId/address changes")
}, [config])

useMemo 做的事:缓存上一次创建的对象

  • 如果 chainId/address 没变:useMemo 直接把上一次那个对象返回给你(引用不变)

  • 如果 chainId/address 变了:useMemo 才会创建新对象(引用改变)

所以变成:

  • 第1次渲染:config 指向 0xAAA

  • 第2次渲染(theme 变了,但 chainId/address 没变):useMemo 返回旧的 0xAAA

  • 第3次渲染(别的 state 变了):还是 0xAAA

  • 直到某次 chainId/address 变了:才生成 0xBBB

React 看依赖 [config]:

“引用没变 → 依赖没变 → effect 不执行。”

6) 用一句超级直观的话总结差别

  • 没有 useMemo:每次渲染都“new 一个 config” → useEffect 觉得依赖变了 → 每次都跑

  • 有 useMemo:只有当 chainId/address 变时才“new config” → useEffect 只在真正变化时跑

7) 你可能会问:那我为什么不直接写  [chainId, address] ?

对,这个问题问得很对。

很多时候最简单就是这样:

useEffect(() => {
  console.log("run only when chainId/address changes")
}, [chainId, address])

那为什么还要 config + useMemo?

常见原因:

  1. 你下游需要一个 config 对象传给别的 hook / 子组件(比如 wagmi/viem)

  2. 依赖项很多,你想统一打包成一个对象(方便维护)

  3. 子组件是 React.memo,你需要保证 props 引用稳定

8) 一个最能让你“秒懂”的类比

  • 没有 useMemo:你每次渲染都“重新打印一张身份证”,身份证号码当然变 → 系统认为换人了 → effect 触发

  • 有 useMemo:只要信息没变,你就一直用同一张身份证 → 系统认为还是同一个人 → effect 不触发

9) useMemo vs useCallback:别混

  • useMemo:缓存“值”

  • useCallback:缓存“函数”

    其实你可以理解为:

useCallback(fn, deps) === useMemo(() => fn, deps)

一个缓存结果,一个缓存函数引用。

10) 实战

下面介绍 6 个真实项目里最常见、而且“用不用 useMemo 差别非常明显”的例子(每个都说明:不用会怎样,用了会怎样)。

例子 1:对象作为 useEffect 依赖导致重复请求(拉余额/交易)

不用 useMemo(会重复请求)

const params = { address, chainId }

useEffect(() => {
  fetchBalance(params) // 可能每次渲染都请求
}, [params])

只要组件因为别的 state 改了(主题、输入框、弹窗),params 就是新对象 → effect 又跑 → 又请求。

用 useMemo(只在 address/chainId 变时请求)

const params = useMemo(() => ({ address, chainId }), [address, chainId])

useEffect(() => {
  fetchBalance(params)
}, [params])

例子 2:给 wagmi/viem 传 config,对象不稳定导致 hook 重复执行/重复订阅

很多 Web3 hook 内部会看 config 是否变化来重建 watcher。

不用 useMemo(可能反复重建 watcher / 重跑)

const config = { address, abi, functionName: "balanceOf", args: [user] }
useReadContract(config)

用 useMemo(config 稳定,只有关键参数变才重建)

const config = useMemo(() => ({
  address,
  abi,
  functionName: "balanceOf",
  args: [user],
}), [address, abi, user])

useReadContract(config)


例子 3:列表过滤/排序很大,导致 UI 卡(交易列表、NFT 列表)

不用 useMemo(任何状态变化都重新 filter/sort)

const visibleTxs = txs
  .filter(tx => tx.chainId === chainId)
  .sort((a,b) => b.time - a.time)

你一切换 theme、打开弹窗、输入搜索框,都会重新跑这套逻辑 → 卡。

用 useMemo(只在 txs 或 chainId 变时重算)

const visibleTxs = useMemo(() => {
  return txs
    .filter(tx => tx.chainId === chainId)
    .sort((a,b) => b.time - a.time)
}, [txs, chainId])


例子 4:把 derived data(派生数据)传给 React.memo 子组件,避免子组件无意义重渲

不用 useMemo(子组件每次都渲染)

const columns = [
  { key: "hash", title: "Hash" },
  { key: "status", title: "Status" },
]

return <Table columns={columns} /> // Table 即使 memo,也会因为 columns 引用变而重渲

用 useMemo(columns 引用稳定,Table 不会乱渲)

const columns = useMemo(() => ([
  { key: "hash", title: "Hash" },
  { key: "status", title: "Status" },
]), [])

return <Table columns={columns} />


例子 5:useEffect 依赖一个 options 对象(比如图表/编辑器/SDK 初始化),导致重复初始化

比如初始化一个 chart/播放器/websocket SDK。

不用 useMemo(effect 每次 render 都 cleanup + init)

const options = { theme: dark ? "dark" : "light", locale }

useEffect(() => {
  const chart = createChart(options)
  return () => chart.destroy()
}, [options])

用 useMemo(只在 theme/locale 真变时重建)

const options = useMemo(() => ({
  theme: dark ? "dark" : "light",
  locale,
}), [dark, locale])

useEffect(() => {
  const chart = createChart(options)
  return () => chart.destroy()
}, [options])


例子 6:依赖数组(args)不稳定导致 effect/hook 频繁触发

不用 useMemo(args 每次 render 都是新数组)

const args = [spender, amount]

useEffect(() => {
  prepareTx(args)
}, [args])

用 useMemo(args 稳定)

const args = useMemo(() => [spender, amount], [spender, amount])

useEffect(() => {
  prepareTx(args)
}, [args])

你该怎么判断“到底要不要用 useMemo”

✅ 用在这两类上最值:

  1. 你把对象/数组/函数放进依赖数组(useEffect/useMemo/useCallback deps)

  2. 你做了重计算(filter/sort/map 大数据/复杂转换)

❌ 别用在:

  • 简单计算(a+b、拼字符串)

  • 没有依赖问题也没有卡顿问题的地方

11) 什么时候别用?

useMemo 不是“越多越好”,它本身也有开销(要维护缓存、比较 deps)。

别用在:

  • 计算很轻(a+b、简单 map)

  • 你根本没遇到性能问题/重复触发问题

  • deps 写不对反而更难 debug

12) 总结

useMemo = 缓存一个值(尤其是慢计算结果 或 需要稳定引用的对象/数组),依赖不变就复用。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐