资讯动态

React.memo性能优化实战:避开失效坑,用对useCallback与useMemo

发布时间:2026/10/16 15:19:48 来源:尧图企业网站定制
我得先说明一点写这篇“memo”笔记不是想在概念上再翻译一遍文档。文档已经写得很清楚了但文档不会告诉你的是——什么时候你加了 memo 反而把代码搞得更乱什么时候你以为 memo 生效了实际上它一直在悄悄失效以及为什么团队里有些人能靠 memo 把页面救活有些人加了三十个 memo 页面还是该卡的卡。这些才是真正值得记下来的东西。1. 什么情况下 memo 能真正帮你省渲染开销1.1 memo 不是组件加载器它只是缓存了一层“上一次的渲染结果”很多人第一次看 React.memo 都有个错觉它有点像之前的 PureComponent或者某种组件级别的“加载优化器”。其实它的行为特别简单React.memo(Component) 会返回一个新组件这个新组件在重新渲染前会拿新的 props 和上一次的 props 做一次浅比较。如果比较后发现所有 props 的值都没变就直接复用上一次的渲染结果不再重新执行组件函数。注意这里有个关键点它缓存的是“上一次渲染结果的复用能力”而不是“把组件实例冻住”。组件内部自己的 state 一变照样要重新渲染组件订阅的 Context 变了照样要重新渲染。memo 唯一拦住的是来自父组件的 props 层面的变化。我记得第一次理解这个机制时拿生活里的快递柜做类比特别容易记memo 像是快递柜上的一次性密码锁只有当你这次投递的物品和新来物品“内容和位置完全一致”时才不用重新开柜直接把上回的东西拿给你。如果里面有哪怕一件物品变了整个柜门都要重新走一遍流程。所以判断 memo 有没有用先想清楚你的子组件是不是经常收到“内容没变但引用变了”的 props如果是那就非常值得加。1.2 判断要不要用的简单标准子组件渲染成本 vs 父组件重渲染频率我在项目里常用的判断标准不是“它是子组件就要 memo”而是两个问题同时成立才值得加第一这个子组件的渲染成本高得明显。比如内部有大量节点、有复杂计算、有长列表的子项、图表组件或者被包了好几层 HOC 的组件渲染一次可能要去掉几毫秒甚至几十毫秒。第二父组件的重渲染频率高且传给子组件的大部分 props 其实没有变化。最典型的场景是父组件里有输入框、有定时器、有网络请求状态位每次输入、每次轮询都会让父组件重渲染。如果子组件只是展示一个表格、一个图表、一组静态配置项那它就是 memo 的理想对象。反过来如果子组件本身渲染只要十几微秒父组件重渲染也不频繁那加 memo 的意义就比较有限了——不仅要写比较逻辑还增加了代码阅读成本属于“功能上没错收益上鸡肋”的优化。另外还要提醒一点memo 比较的对象是 props不是 state所以它挡不住父组件重渲染时子组件内部 state 变化带来的渲染。这种场景就不能指望 memo而是要从数据流结构去解决。2. memo 原理一次点击背后发生的四步判断2.1 浅比较到底比了什么props 对象的 key 和 value很多人用 memo 的时候心里其实没有“浅比较”这个概念觉得只要 props 看着没变就一定会命中缓存。这是最危险的误解。React 对 memo 组件的比较逻辑可以拆成四步旧 props 对象和新 props 对象拿出来。遍历新旧 props 的所有 key。对每个 key 执行 Object.is 比较对应的 value。只要有一个 key 的 value 不相同就判定为 props 变了需要重新渲染。请注意第 3 步里的 Object.is。对于基本类型比如字符串、数字、布尔值、null、undefined那确实是一比一个准。但引用类型立马露出马脚两个看起来内容完全一样的对象、数组、函数只要不是同一个引用Object.is 就会返回 false。举个实际例子父组件里这样写Child config{{ theme: dark, size: md }} /父组件每次重渲染这个{ theme: dark, size: md }都是一个全新的对象字面量。哪怕值完全一样在浅比较这一步新旧 value 也不是同一个引用结果就是判定 props 变了memo 的复用直接失效。所以你可以把 memo 的浅比较理解为“比钥匙不比门”它只关心你拿的是不是同一把钥匙不关心这把钥匙能不能开同一扇门。2.2 函数作为 props 时浅比较为什么必然失效更隐蔽的坑是函数 props。父组件内联写Child onSave{() saveData()} /每次父组件重渲染都会创建一个新的箭头函数。这个函数跟上一轮的函数功能完全相同但引用不同。浅比较一看value 变了于是 memo 又失效。项目里最冤枉的事就是有人给一个子组件加了 memo结果传给它的回调全是用箭头函数内联写的memo 形同虚设性能调查半天找不到原因。这里有一个非常容易忽略的小细节如果子组件内部还用了 useEffect 并把这个回调放进了依赖数组那回调引用一变useEffect 也会跟着重复执行。也就是说内联函数带来的问题不只是 memo 失效还可能引发副作用重复执行。函数稳定性和 memo 是两个绑在一起的问题。想让它俩配合好就需要用 useCallback 把回调的引用稳定下来。关于这点我在后面第 4 节会直接给你一套可抄的写法。3. 我在项目里踩过的 memo 无效的坑3.1 传了一个内联对象相当于每次都在换新钥匙有一回我们做活动页活动卡片组件被 memo 包得很好代码看着完美。父组件里有一块定时器每五秒刷新一次数据剩余时间。然后页面上所有活动卡片都跟着闪根本停不下来。查了半天发现是给卡片组件传了一个对象 propsCard discount{{ type: percent, value: card.discount }} onPress{() handlePress(card.id)} /这个discount是每次都在父组件里新构造的对象。父组件因为定时器每五秒重渲染一次新对象跟着生成memo 浅比较发现引用变了于是每五秒强制所有卡片重新渲染一次。当时我做了两处修复第一用 useMemo 把 discount 对象稳定下来只有当card.discount真正变化时才生成新对象第二onPress 改用 useCallback 包裹以card.id为依赖。改完之后定时器的每次 tick 都不会再引起活动卡片的无谓重渲染页面的滚动和点击明显顺了。这件事给我留下的经验是凡是父组件重渲染时才生成的对象、函数、数组都是 memo 的“假性需求”。你加 memo 之前最好先排查一遍传给子组件的 props 里到底有没有“每次渲染都换新引用”的东西。3.2 把 children 直接传给 memo 组件缓存失效却没发现还有一个特别坑的场景你以为 memo 已经把子组件保护起来了结果父组件里写memoChild span额外内容/span /memoChild这时候有个隐藏规则memo 比较的 props 里面其实包含了一个 children 属性。你写的span传给了 Child如果父组件重渲染时这个span是每次重新创建的 React 元素那么它的引用也在变。React 元素本质上是一个对象每次渲染都会重新创建。浅比较发现 children 的引用变了于是 memo 同样失效。你可能觉得“children 又没变啊”但事实上引用已经变了。解决思路有两种。第一种如果 children 是静态的可以把它提升到组件外部作为常量const staticChildren span额外内容/span; function Parent() { return memoChild{staticChildren}/memoChild; }第二种如果 children 依赖父组件的某些内容那说明这部分 content 本来就跟父组件强相关memo 保护不了也是合理的不如不要试图绕过它直接让子组件接受父组件变化就好。不要太迷信“给组件包了 memo 就等于隔离了父组件”。children 也是个 props它被无视的话隔离就是一道漏风的墙。3.3 数据驱动组件时拿到的回调函数换了引用去年给一个后台管理系统做优化左侧筛选器和右侧表格联动。表格组件包了 memo筛选条件变了会触发父组件重新请求数据再传给表格新数据整个过程很合理。但奇怪的是我没有改筛选条件只是点了一下页面上的一个展开按钮表格也跟着重新渲染了。展开按钮的 state 在父组件里父组件一重渲染表格就重渲染。打开 React DevTools查看表格组件的 props 变化发现数据没变但有一个回调函数变了。原来是父组件里给表格传了onOperation这个回调是直接写的Table data{tableData} onOperation{(row, type) handleOperation(row, type)} /handleOperation是一个稳定的函数但箭头函数把它包了一层每轮渲染都生成新函数。改用useCallback直接包裹handleOperation本体再传给组件问题才解决。类似的问题还会出现在 render props、map 生成的列表项等场景里。总之排查 memo 失效第一件事就是把 DevTools 里 props 的前后引用对比看得明明白白而不是靠肉眼猜“看着没变啊”。4. 结合 useCallback 和 useMemo 的完整解法4.1 稳定回调的核心模式要让 memo 真正起作用光包一层 memo 是不够的需要把它的“前端防线”也修好。前端防线的核心就是把每次渲染都会重新生成的函数和对象稳定下来。先看最简单的函数模式。原本写法Child onSave{() saveData(values)} /稳定写法const handleSave useCallback(() { saveData(values); }, [values]); Child onSave{handleSave} /这里的关键在于依赖数组useCallback 会缓存上一次的回调只有当values变化时才会返回新函数。这样一来只要values没变handleSave的引用就一直稳定Child 组件的 memo 就能命中。注意一个容易迷惑的细节values是父组件 state 的话每次父组件重渲染时values本身引用可能也会变。如果values是一个对象光放进依赖数组还不够还需要在父组件里用 useMemo 稳定它。否则依赖一变useCallback 也跟着变memo 还是活不了。所以完整链路是用 useMemo 稳定对象/数组类型的“数据 props”。用 useCallback 稳定“回调 props”。用 memo 在子组件这一侧做“引用比较”。三者缺一个优化链就断了。4.2 依赖数组里的值也变了怎么办有人会问如果回调确实依赖一个经常变化的值比如输入框内容那 useCallback 还有用吗答案是这种情况下函数引用必须变否则回调里拿到的值就是旧值。这不是 bug是逻辑依赖。你没办法让一个“依赖最新输入值的回调”保持引用稳定除非你换一种设计。我见过很多团队在这里陷入怪圈为了保持引用稳定把依赖数组写歪故意漏掉依赖项。这是大忌。React 的 hooks 是带着记忆的依赖数组漏了值回调就会捕获到上一次的旧值后面所有逻辑都会错乱。真正理性的处理方式是如果某个回调依赖高频变化的值那么依赖这个回调的子组件在“父组件重渲染频率较高且大部分 props 不变”的场景下本来就不太适合用 memo 硬优化。因为高频变化的那部分每次都要重新渲染memo 挡不住反而让代码多一层心智负担。可以做的替代优化是把高频变化的内容和低频变化的内容拆成不同组件。比如输入框组件频繁变表格组件不频繁变让输入框不要触发表格的父容器整体重渲染这比单纯 memo 表格更根本。5. memo 治不了的两类问题以及更实际的选择5.1 状态提升与 Context 导致的“无谓重渲染”memo 能拦父组件的 props但拦不住两类东西一是父组件内部 state 变化触发的自身重渲染二是 Context 值变化触发的所有消费者重渲染。讲个实际经历。项目里有一个筛选面板我把筛选条件放到了父组件 state筛选结果展示在子组件。当用户在筛选面板输入时父组件每敲一个字母就要重渲染子组件就算包了 memo因为输入值变化导致传给子组件的筛选结果也在变还是会重新渲染。真正解决这个问题的办法不是 memo 子组件而是把“输入交互”和“结果展示”拆成两个独立的领域。输入面板的 state 不要放在结果展示的父组件里用局部 state 管理输入通过提交动作再把结果传给上层。这样一来输入过程中的中间状态根本不会波及结果展示组件。Context 也有类似问题。Context 的 value 一旦变化所有 useContext 的组件都会强制重渲染memo 拦不住。解决方向要么是把 Context value 拆成多个小 context要么是用更细粒度的 selector 模式而不是指望 memo。5.2 不要为了全面 memo 而全面 memo我见过最极端的代码项目里所有组件一律包上 memo。这种做法的代价是每一次 props 变化React 都要多执行一次浅比较当 props 比较多、层级比较深时这些比较本身也有成本。更麻烦的是全面 memo 会造成一种假象——组件不会重渲染了。但真正因为业务逻辑导致的数据变化该渲染还是会渲染只是排查问题的时候多了一层不可预期的缓存反而更难追踪。我的习惯是先做性能测量再决定是否 memo。用 React DevTools 的 Profiler 录制典型交互看一下哪个组件实际渲染次数最多、耗时最长然后针对性地给那部分组件加 memo同时配合 useCallback / useMemo 稳定引用。优化这件事最怕的就是“玄学优化”和“安慰剂优化”。加了一个 memo 心里舒坦了但 DevTools 的火焰图告诉你它根本没命中缓存这就说明前面的 props 设计有问题而不是 memo 没用。还有一个小提醒memo 比较完成后如果返回 true复用组件函数不会重新执行这确实能省一部分渲染时间但如果组件内部用了很多非 props 的上下文变量比如全局状态、路由信息、主题变量memo 的收益就会被进一步稀释。离开“props 是否稳定”这个核心条件去谈 memo 优化基本就是在盲人摸象。最后分享一个我自己的排查小技巧遇到页面卡顿不要第一时间加 memo而是先用 Profiler 找到真正高频重渲染的组件然后打开 console 写一段临时日志把它收到的 props 在前后渲染中做一次引用对比。发现哪个 props 引用总在变就从哪里下手。等你把引用稳定下来回头再看通常会发现原本以为需要 memo 的组件根本不需要 memo 也跑得很稳定。React 的性能问题大多数时候不是缺少缓存而是数据流的引用设计出了问题memo 不过是一道补救阀而已。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑