最近在优化一个 React 项目的时候,使用了 React.memo 来避免不必要的组件渲染,但发现有时候 React.memo 并没有生效,导致组件仍然会重新渲染,带来了不小的性能问题。针对这个问题,我进行了深入的排查和研究,总结了一些常见的失效原因和解决方案。
问题场景重现
假设我们有一个简单的组件,它接收一个 name 和一个 onClick 属性:
import React from 'react';
const MyComponent = React.memo(function MyComponent({ name, onClick }) {
console.log('MyComponent 渲染了!'); // 用于观察渲染情况
return (
<button onClick={onClick}>Hello, {name}</button>
);
});
export default MyComponent;
在父组件中使用它:
import React, { useState, useCallback } from 'react';
import MyComponent from './MyComponent';
function App() {
const [count, setCount] = useState(0);
const [name, setName] = useState('World');
const handleClick = useCallback(() => {
setCount(count + 1);
}, [count]);
const handleNameChange = (e) => {
setName(e.target.value);
};
return (
<div>
<p>Count: {count}</p>
<input type="text" value={name} onChange={handleNameChange} />
<MyComponent name={name} onClick={handleClick} />
</div>
);
}
export default App;
按理说,只有当 name 属性发生变化时,MyComponent 才应该重新渲染。但实际上,每次点击按钮,即使 name 没有改变,MyComponent 也会重新渲染。
底层原理深度剖析
React.memo 的作用是浅比较组件的 props,只有当 props 发生变化时,才会重新渲染组件。如果 props 是基本类型(如字符串、数字),浅比较会直接比较值是否相等。但如果 props 是引用类型(如对象、函数),浅比较只会比较引用地址是否相等。这就导致了以下几种 React.memo失效 的常见原因:
函数组件每次渲染都会创建新的函数实例:在上面的例子中,
handleClick函数在每次App组件重新渲染时都会创建一个新的函数实例,即使它的逻辑完全相同。因此,MyComponent接收到的onClick属性的引用地址总是不同的,导致React.memo失效。对象字面量作为 props:如果直接在 JSX 中使用对象字面量作为 props,每次渲染都会创建新的对象,导致
React.memo失效。props 传递过深,导致中间组件更新触发子组件更新:即使使用了
React.memo,如果父组件因为其他原因重新渲染,导致MyComponent的父组件重新渲染,MyComponent也会被标记为需要更新。使用
useContext且 context 值频繁变化:useContext会导致组件在 context 值发生变化时重新渲染,即使 props 没有改变,也会导致React.memo失效。这在大型应用中尤为需要注意,尤其涉及到全局状态管理,例如 Redux 或者 MobX,需要考虑 Immutable Data Structures 的应用。
具体的代码/配置解决方案
针对上述问题,可以采取以下解决方案:
使用
useCallback缓存函数:使用
useCallback可以缓存函数,避免每次渲染都创建新的函数实例。将上面的handleClick函数修改为:const handleClick = useCallback(() => { setCount(count + 1); }, [count]); // 依赖 count这样,只有当
count发生变化时,handleClick函数才会更新,否则MyComponent接收到的onClick属性的引用地址保持不变。
避免使用对象字面量作为 props:
将对象字面量提取到组件外部,或者使用
useMemo缓存对象。// 错误示例 <MyComponent style={{ color: 'red' }} /> // 正确示例 const style = useMemo(() => ({ color: 'red' }), []); <MyComponent style={style} />优化组件结构,避免不必要的渲染:
尽量减少组件之间的依赖关系,避免父组件的更新导致子组件的更新。可以使用
useMemo缓存计算结果,避免重复计算。
检查
useContext的使用: 确保Context Provider 的 value 没有不必要的更新。使用 Immutable data structures 可以帮助你追踪状态的变化。
实战避坑经验总结
养成良好的编码习惯:尽量避免在 JSX 中直接使用对象字面量和函数字面量。对于需要传递给子组件的函数,尽可能使用
useCallback缓存。使用 React DevTools 观察组件渲染情况:React DevTools 可以帮助你观察组件的渲染情况,快速定位性能问题。
谨慎使用
React.memo:React.memo并非万能药,过度使用反而可能带来性能问题。只在确定组件渲染代价较高,且 props 变化频率较低时才使用React.memo。关注依赖项:
useCallback和useMemo的依赖项非常重要,确保依赖项列表正确,避免不必要的更新或者缓存失效。如果依赖项太多,可以考虑使用useReducer来管理状态。服务端渲染 SSR 注意事项:在服务端渲染环境中,由于没有真实的 DOM,
useLayoutEffect会发出警告。可以使用useEffect代替,或者使用专门针对 SSR 的解决方案。
总而言之,理解 React.memo 的工作原理,避免常见的失效原因,结合 React DevTools 等工具进行性能分析,才能真正发挥 React.memo 的作用,提升应用的性能。在使用 Next.js 这类 SSR 框架时,还需要关注服务端渲染的特殊性,避免出现兼容性问题。
冠军资讯
脱发程序员