- Published on
React Hook 专题:useMemo
- Authors

- Name
- 葛颂
useMemo 在依赖不变时跳过昂贵计算,复用上一次的结果。它是性能优化手段,不是「这个值绝对不变」的语义承诺——React 在特定情况下可以丢弃缓存(见 官方说明)。
若项目启用了 React Compiler,多数手动 useMemo 可交给编译器;没有 profiler 证据时,不必默认包一层。
基本用法
import { useMemo } from 'react'
function ProductList({ products, filterText }: Props) {
const visible = useMemo(() => {
return products.filter((p) => p.name.includes(filterText))
}, [products, filterText])
return visible.map((p) => <Row key={p.id} product={p} />)
}
签名:
const cached = useMemo(() => compute(), dependencies)
- 首次渲染会执行
compute并缓存返回值。 - 之后仅当依赖中某一项
Object.is比较发生变化时,才重新计算。
原理
渲染阶段:
- React 比对依赖数组与上次存储的依赖。
- 全相等 → 直接返回缓存值,不跑
compute。 - 有变化 → 执行
compute,写入新缓存。
注意:
compute应是纯函数(同样输入 → 同样输出,无副作用)。副作用应放在事件或useEffect里。- 缓存存在 Fiber 上,与这次组件实例绑定;卸载后消失。
- 开发热更新、初始 mount 时 suspend 等场景下,React 可能丢掉缓存再算一遍。因此:不要把「只算一次」的业务语义押在
useMemo上;那种需求更适合useState惰性初始化或useRef。
什么时候值得用
官方常见动机:
- 计算明显很贵(大列表 filter/sort、复杂可视化数据),且 profiler 证明卡顿。
- 稳定引用,以便子组件被
memo包住后能跳过重渲染。 - 作为其他 Hook 的依赖(另一个
useMemo/useEffect),避免每次渲染新建对象导致下游无效刷新。
不值得用的情况:
- 简单算术、短数组 map、字符串拼接——memo 自身的比较成本可能更高。
- 「以后可能会慢」的过早优化。
- 用
useMemo伪装「派生 state」却在里面setState(错误模型)。
// ❌ 几乎总是不需要
const fullName = useMemo(() => `${first} ${last}`, [first, last])
// ✅
const fullName = `${first} ${last}`
与「渲染时直接算」的关系
派生数据的第一选择永远是渲染期间计算。只有测量后确认昂贵,再加 useMemo:
// 默认
const sorted = [...items].sort(compare)
// 确认昂贵后再包
const sorted = useMemo(() => [...items].sort(compare), [items])
常见问题与解决办法
1. 依赖写错:该更新时不更新 / 不该更新时狂算
漏依赖:缓存了过期结果(和 useEffect 的陈旧闭包同类问题)。
多余依赖:每次渲染都变的对象/函数写进数组 → useMemo 形同虚设。
办法:用 exhaustive-deps;把「每次新建的 options 对象」改为依赖原始字段,或在更外层稳定引用。
// ❌ filter 每次都是新函数 → memo 永远失效
useMemo(() => items.filter(filter), [items, filter])
// ✅ 依赖原始条件
useMemo(() => items.filter((x) => x.type === type), [items, type])
2. 把 useMemo 当成「只执行一次」
// ❌ 语义错误:React 可以丢弃缓存
const id = useMemo(() => crypto.randomUUID(), [])
办法:
const [id] = useState(() => crypto.randomUUID())
// 或
const idRef = useRef(null)
if (idRef.current === null) idRef.current = crypto.randomUUID()
3. 在 useMemo 里做订阅 / 请求
计算属性被缓存跳过时,副作用不会跑;依赖变化时又可能重复订阅。这是错误分层。
办法:副作用 → useEffect;纯计算 → useMemo / 直接算。
4. 为了 memo 子组件而 memo 一切 props
父组件里一堆 useMemo,可读性变差,收益却接近零。
办法:
- 先确认子组件是否真用了
memo,以及重渲染是否可测。 - 能把 state 下移、内容拆分就不传大对象。
- 有 React Compiler 时,优先让编译器处理。
5. 返回值仍是「内部新引用」
// 外层 memo 住了,但每次仍返回新对象字面量……等一下,这是 compute 的结果
const style = useMemo(() => ({ color }), [color]) // ✅ 依赖不变则引用不变
const style = useMemo(() => ({ color, nested: { a: 1 } }), [color])
// nested 每次重算都是新的——但整体 style 在 color 不变时仍是同一引用 ✅
真正的问题是:没有 useMemo 时把内联对象传给 memo 子组件:
<Chart style={{ color }} /> // 每次新对象 → memo 失效
此时才需要 useMemo(或把 style 变成原始值 prop)。
6. 依赖数组「省略」或传错类型
必须传数组。忘记传依赖等于「每次都重算」,还容易误导读者以为已缓存。
实践清单
- 默认:渲染时直接算。
- Profiler 证明瓶颈,或要稳定引用给
memo/ 下游 Hook 时,再加useMemo。 compute保持纯净;不做 I/O。- 不依赖「缓存永不失效」的语义。
- 有 Compiler 时,少写手动 memo。