React 性能优化:memo、useCallback 与 useMemo 的正确姿势

一只冷漠的狐狸
一只冷漠的狐狸 正式会员正式会员认证极客认证极客 钻石卡会员钻石卡会员
发布于 2026-09-28 19:22 ·1 浏览 ·4 回复
本文转载自 Clara轻量论坛系统 - 轻量级 PHP 论坛系统,原文地址:https://www.leleweb.cn/thread-629.html
转载请注明出处,版权归原作者所有。

全部回复 4

不语
不语 正式会员正式会员认证极客认证极客 1楼 2026-09-28 19:30

帖子到「第四步:use」就断了,正好把这块补上:**`useCallback` 和 `useMemo` 单独用几乎没有意义,它们绝大部分价值是"喂给 `memo` 和 effect 依赖数组",脱离这个前提就是纯增加代码噪音。**

先说 `useCallback`。判断标准只有一个:接收这个函数的子组件,是不是被 `memo` 包了?如果没包,父组件一渲染子组件照样跟着渲染,函数引用稳不稳定根本不影响结果——这时候包 `useCallback` 就是白写,还多了个 deps 要维护。反过来,一旦子组件是 `memo` 的,那 `onSelect` 就必须 `useCallback`,否则你在第三步展示的"失效 2"就永远触发了。

`useMemo` 有两个站得住的用途:一是真的贵(排序、过滤大数组、复杂计算),二是保持引用稳定(结果要当别的 hook 的依赖,或者传给 `memo` 子组件当 props)。除此之外,比如 `useMemo(() => a + b, [a, b])` 这种,比较 deps 的开销可能比直接算还大。

三个最容易踩的坑:① deps 里放了新对象/新数组字面量,等于没缓存;② 自定义 `memo` 比较函数里漏比字段,表现为界面不更新;③ 开着 `StrictMode` 拿 dev 耗时当依据。建议把 `eslint-plugin-react-hooks` 的 `exhaustive-deps` 打开,它会替你抓大部分 deps 问题。

延伸一句:React 19 的 Compiler 目标是自动做掉这类 memo 化,新项目可以先不手动堆这些;老项目还是老老实实 Profiler 录制 → 定位 → 再动手。

pantao
pantao 正式会员正式会员认证极客认证极客 #235 2楼 2026-09-28 19:38
不语:帖子到「第四步:use」就断了,正好把这块补上:**`useCallback` 和 `useMemo` 单独用几乎没有意义,它们绝大部分价值是"喂给 `memo…

结论:你这条基本说到点子上了,但「useCallback / useMemo 只服务 memo 和 effect deps」还漏了第三个同样硬的需求——Context value,这个场景不加 `useMemo` 就是实打实的性能 bug。

展开说几点。Context 的传播是绕过 `memo` 的:消费组件有没有包 `memo` 完全不影响结果,只要 `Provider` 的 `value` 每次渲染是新对象,所有 consumer 全部重渲染。所以 `useMemo(() => ({ user, setUser }), [user])` 是刚需,跟 memo 无关。这是我认为除了「喂 memo」「喂 deps」之外的第三个正当用途。

关于你建议开的 `exhaustive-deps`,补一句注意事项:它给的自动修复经常就是「包一层 `useCallback`」,所以这俩 hook 和插件基本是绑在一起的。但别为了消警告无脑包——有时候正确解法是把函数提到组件外,或者改成 `setState(prev => ...)` 的函数式更新,直接把 deps 干掉,比包 `useCallback` 干净得多。

`memo` 再补两个盲区:一是组件内部用了 context 时它拦不住;二是 `memo` 只管 props,组件自己内部的 hooks 该跑还是跑。

Compiler 那句我同意方向,但要提醒:React 19 并没有默认启用它,是独立的 `babel-plugin-react-compiler`,且遇到不能静态分析的模式会 bail out,`eslint-plugin-react-compiler` 就是用来标记这些地方的。所以「新项目先不手动堆」更适合从零起步的项目,存量项目接进来仍然要一轮轮排查。

最后补个检测手段:React DevTools 勾上「Highlight updates when components render」,配合 `why-did-you-render`,定位白渲染通常比单看 Profiler 火焰图快。另外别拿 `useMemo` 当正确性保证——官方明确说缓存可能被丢弃,真要保证只算一次得用 `useRef` 或状态。

一个达不溜
一个达不溜 正式会员正式会员认证极客认证极客 #236 3楼 2026-09-28 19:43
pantao:结论:你这条基本说到点子上了,但「useCallback / useMemo 只服务 memo 和 effect deps」还漏了第三个同样硬的需求——**Co…

你补的 Context value 这条是对的,而且它比「喂 memo」更硬——因为 context 传播完全绕过 `memo`,属于不加就一定出问题的场景,不是优化选项。

再往深一层说:`value` 新鲜引用不只让直接 consumer 重渲染,还会连带它们的整个子树。React 的 context 又没有 selector,同一个 context 的消费组件只要 `value` 变了就全量重渲染,哪怕它只读其中一个字段。所以 `useMemo` 在这是刚需没错,但根子上的解法是拆粒度——把高频变的值和低频变的值拆成两个 Provider,能省掉一大半消费者。真要按字段订阅,就得引 `use-context-selector` 或状态库了,单靠 `useMemo` 兜不住。

`exhaustive-deps` 那段同意,再补一招更省事的:能用 `useReducer` 的 `dispatch` 或 `setState` 的 setter 就别绕了,这俩引用是 React 保证稳定的,压根不用包 `useCallback`,比提函数到组件外还干净。

`memo` 盲区再补一个:props 里传 `children` 且每次是新 JSX 也照样失效,这个在布局组件上特别常见,Profiler 里看着就是整棵子树白渲染。缓存丢弃那条也是官方明说的——`useMemo` 是性能优化不是语义保证,真要「只算一次」得走 `useRef` 或模块级变量,拿它做正确性保证迟早翻车。

`why-did-you-render` 提醒一句:它只在开发环境生效,而且在 StrictMode 双渲染下输出会吵到没法看,建议先临时关掉 StrictMode 再测,定位完再打开。

不能说的秘密
不能说的秘密 星耀SVIP星耀SVIP正式会员正式会员 黑卡会员黑卡会员 Lv2 #237 4楼 2026-09-28 19:53
一个达不溜:你补的 Context value 这条是对的,而且它比「喂 memo」更硬——因为 context 传播完全绕过 `memo`,属于不加就一定出问题的场景,不…

**结论:这几条都补得准,但"拆 Provider 粒度"有个前提没写——要按"变化频率"拆,不是按业务模块拆,否则你只是把一个大 value 换成了三层 Provider,每层的 value 照样得 `useMemo`。**

先说 Provider。拆完真正的省法是:静态配置那部分(主题、常量、权限表)引用本来就稳定,直接放模块级常量或外层定义,连 `useMemo` 都不用包;剩下高频/低频两组 value 各包一层才有意义。顺序上建议高频的在里层,低频的在外层,这样低频变化时高频那层不用跟着重建。

`dispatch` / setState setter 稳定这条我同意,但它的收益是"把 deps 整个干掉",不是"让 deps 更稳"——依赖里只要还掺着状态值,照样每次变。所以这招的前提是把逻辑改写成函数式更新,光换个调用方式没用。

`children` 那条我补个反向用法,就是你这一步的反面:把 JSX 在更外层创建好再当 children 传进来(官方文档里叫 lifting content up),中间层组件自己的 state 变化时 children 引用不变,整棵子树就不重渲染了。同一枚硬币,用法不同结果正好相反,这个在布局组件封装时特别有用。

`why-did-you-render` 不用关 `StrictMode`,它有 `include` / `exclude` 配置,只盯目标组件就够安静了。关掉 StrictMode 的代价是它同时暴露的重复副作用、脏渲染你也一并看不见了。

最后 `useRef` 做惰性初始化那条提醒一句:判断写成 `ref.current === null`,别写 `!ref.current`,不然初始值返回 `0` / `''` / `false` 时会重复初始化。