【React-10/Lesson94(2026-01-04)】React 性能优化专题:useMemo & useCallback 深度解析🚀
📚 React 性能优化的重要性
在 React 应用开发中,性能优化是一个不可忽视的重要话题。随着应用规模的不断增长,组件的渲染效率直接影响着用户体验。React 提供了多种性能优化方案,其中 useMemo 和 useCallback 是两个最常用的 Hook,它们能够帮助我们避免不必要的重复计算和渲染,从而提升应用的性能表现。
🔍 includes 方法详解
在深入性能优化之前,我们先来了解一下 includes 方法,这是 JavaScript 字符串和数组中常用的方法。
字符串 includes 方法
"apple".includes("") === true // 空字符串也为 true
"apple".includes("app") === true // 包含子字符串也为 true
"apple".includes("ab") === false // 不包含子字符串为 false
includes() 方法用于判断一个字符串是否包含另一个字符串,返回布尔值。需要注意的是:
- 空字符串
""总是被认为包含在任何字符串中,所以"apple".includes("")返回true - 该方法区分大小写,
"Apple".includes("apple")返回false - 支持第二个参数,表示从哪个位置开始搜索
数组 includes 方法
[1, 2, 3].includes(2) === true
[1, 2, 3].includes(4) === false
['apple', 'banana'].includes('apple') === true
数组版本的 includes() 方法用于判断数组中是否包含某个元素,同样返回布尔值。
⚠️ React 中的性能陷阱
在 React 组件中,我们需要特别注意避免在 render 过程中进行复杂的计算。让我们看一个典型的例子:
const [count, setCount] = useState(0);
const [keyword, setKeyword] = useState('');
const list = ['apple', 'banana', 'orange', 'pear'];
const filterList = list.filter(item => item.includes(keyword));
在这个例子中,当 count 改变时,整个组件会重新渲染,filterList 也会重新执行,即使 keyword 并没有发生变化。这就是一个典型的性能问题。
问题根源
React 组件的状态更新会触发组件的重新渲染,这意味着:
setCount(count + 1); // filterList 会重新执行,即使它与 count 无关
每次组件重新渲染时,所有的计算逻辑都会重新执行,包括那些与状态变化无关的计算。当计算逻辑比较复杂时,这就会造成明显的性能问题。
💡 useMemo:缓存计算结果
useMemo 是 React 提供的一个 Hook,用于缓存计算结果,避免在每次渲染时都进行重复计算。
useMemo 的基本语法
const memoizedValue = useMemo(() => {
return computeExpensiveValue(a, b);
}, [a, b]);
- 第一个参数:一个函数,返回需要缓存的值
- 第二个参数:依赖数组,只有当数组中的依赖项发生变化时,才会重新计算
昂贵计算的优化示例
function slowSum(n) {
console.log('计算中...');
let sum = 0;
for (let i = 0; i < n * 10000000; i++) {
sum += i;
}
return sum;
}
const [num, setNum] = useState(0);
const result = useMemo(() => {
return slowSum(num);
}, [num]);
在这个例子中,slowSum 是一个计算密集型的函数。使用 useMemo 缓存其结果后,只有当 num 发生变化时才会重新计算,避免了不必要的重复计算。
列表过滤的优化
const [keyword, setKeyword] = useState('');
const list = ['apple', 'banana', 'orange', 'pear'];
const filterList = useMemo(() => {
return list.filter(item => item.includes(keyword));
}, [keyword]);
通过 useMemo 缓存 filterList,只有当 keyword 发生变化时才会重新执行过滤操作,其他状态的变化不会触发这个计算。
useMemo 与 Vue computed 的对比
React 的 useMemo 与 Vue 的 computed 计算属性功能类似:
- 都是用于缓存计算结果
- 都基于依赖项进行缓存更新
- 都能避免不必要的重复计算
但也有一些区别:
-
useMemo需要显式指定依赖数组 -
computed会自动追踪依赖 -
useMemo的粒度更细,可以缓存任意值
🎯 useCallback:缓存函数引用
useCallback 是另一个重要的性能优化 Hook,它用于缓存函数引用,避免在每次渲染时都创建新的函数实例。
useCallback 的基本语法
const memoizedCallback = useCallback(() => {
doSomething(a, b);
}, [a, b]);
- 第一个参数:需要缓存的回调函数
- 第二个参数:依赖数组,只有当依赖项发生变化时,才会返回新的函数引用
为什么需要 useCallback
在 React 中,函数作为 props 传递给子组件时,每次父组件渲染都会创建新的函数实例:
const handleClick = () => {
console.log('click');
};
<Child count={count} handleClick={handleClick} />
即使 handleClick 的逻辑没有变化,每次渲染都会创建一个新的函数引用,这会导致子组件即使使用了 memo 也会重新渲染。
useCallback 的使用示例
const handleClick = useCallback(() => {
console.log('click');
}, [count]); // 依赖项 count 变化时,才会重新创建 handleClick 函数
通过 useCallback 缓存函数引用,只有当依赖项发生变化时才会创建新的函数实例,避免了不必要的子组件重新渲染。
🏗️ memo 高阶组件
memo 是 React 提供的一个高阶组件,用于优化函数组件的性能,避免不必要的重新渲染。
memo 的基本用法
const Child = memo(({count, handleClick}) => {
console.log('child 重新渲染');
return (
<div onClick={handleClick}>
子组件{count}
</div>
);
});
memo 的工作原理
memo 会对组件的 props 进行浅比较:
- 如果 props 没有变化,组件不会重新渲染
- 如果 props 发生变化,组件会重新渲染
memo 与 useCallback 的配合使用
const Child = memo(({count, handleClick}) => {
console.log('child 重新渲染');
return (
<div onClick={handleClick}>
子组件{count}
</div>
);
});
const handleClick = useCallback(() => {
console.log('click');
}, [count]);
只有当 count 发生变化时,handleClick 才会重新创建,Child 组件才会重新渲染。
🔄 React 数据流管理思想
React 的数据流管理思想强调:
- 父组件负责持有数据和管理数据
- 子组件负责根据数据渲染 UI
这种单向数据流的设计使得状态管理更加清晰和可预测。当某一个数据改变时,我们只想让相关的子组件重新渲染,而不是所有子组件。
状态管理的最佳实践
export default function App() {
const [count, setCount] = useState(0); // 响应式业务1——计数
const [num, setNum] = useState(0); // 响应式业务2——计数
const handleClick = useCallback(() => {
console.log('click');
}, [count]);
return (
<div>
{count}
<button onClick={() => setCount(count + 1)}>count+1</button>
{num}
<button onClick={() => setNum(num + 1)}>num+1</button>
<Child count={count} handleClick={handleClick} />
</div>
);
}
在这个例子中,count 和 num 是两个独立的状态,各自负责各自的业务逻辑。通过 useCallback 缓存函数,我们可以精确控制子组件的渲染时机。
📊 高阶组件与高阶函数
理解高阶组件和高阶函数的概念,有助于我们更好地掌握 React 的性能优化技巧。
高阶组件(HOC)
高阶组件是一个函数,它的参数是一个组件,返回值是一个新的组件:
function withLoading(WrappedComponent) {
return function(props) {
if (props.isLoading) {
return <div>Loading...</div>;
}
return <WrappedComponent {...props} />;
};
}
const EnhancedComponent = withLoading(MyComponent);
高阶函数
高阶函数是一个函数,它的参数是一个函数,返回值是一个新的函数:
function withLogger(fn) {
return function(...args) {
console.log('Calling function with args:', args);
return fn(...args);
};
}
const enhancedFn = withLogger(myFunction);
对比记忆
- 高阶组件:参数是组件,返回新组件
- 高阶函数:参数是函数,返回新函数
memo 就是一个典型的高阶组件,它接收一个组件作为参数,返回一个优化后的组件。
🎯 性能优化的最佳实践
何时使用 useMemo
-
昂贵的计算:当计算逻辑比较复杂时,使用
useMemo缓存结果 -
依赖其他状态的计算:当计算结果依赖于某些状态时,使用
useMemo避免不必要的重新计算 -
引用类型的依赖:当计算结果作为其他 Hook 的依赖时,使用
useMemo保持引用稳定
何时使用 useCallback
-
传递给子组件的回调函数:当函数作为 props 传递给子组件时,使用
useCallback缓存函数引用 -
作为其他 Hook 的依赖:当函数作为其他 Hook 的依赖时,使用
useCallback保持引用稳定 -
事件处理函数:当函数作为事件处理函数时,使用
useCallback避免重复创建
何时使用 memo
-
纯展示组件:当组件主要功能是展示数据,不涉及复杂逻辑时,使用
memo优化性能 -
频繁渲染的父组件:当父组件频繁渲染,但子组件的 props 变化不频繁时,使用
memo避免不必要的重新渲染 -
大型列表项:当渲染大型列表时,对列表项使用
memo可以显著提升性能
⚡ 性能优化的注意事项
避免过度优化
虽然性能优化很重要,但也要避免过度优化:
- 不要对所有组件都使用
memo - 不要对所有计算都使用
useMemo - 不要对所有函数都使用
useCallback
只有在确实存在性能问题时,才应该进行优化。
依赖数组的正确使用
使用 useMemo 和 useCallback 时,要正确设置依赖数组:
// 错误示例:遗漏依赖
const handleClick = useCallback(() => {
console.log(count);
}, []); // 应该包含 count
// 正确示例:包含所有依赖
const handleClick = useCallback(() => {
console.log(count);
}, [count]);
使用 ESLint 插件
推荐使用 eslint-plugin-react-hooks 插件,它会自动检查依赖数组的正确性:
{
"plugins": ["react-hooks"],
"rules": {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
}
🔧 实际应用场景
场景1:复杂的数据处理
const processedData = useMemo(() => {
return rawData
.filter(item => item.active)
.map(item => ({
...item,
formattedDate: formatDate(item.date),
calculatedValue: complexCalculation(item.value)
}))
.sort((a, b) => b.calculatedValue - a.calculatedValue);
}, [rawData]);
场景2:表单验证
const validateForm = useCallback((values) => {
const errors = {};
if (!values.email) {
errors.email = 'Email is required';
} else if (!isValidEmail(values.email)) {
errors.email = 'Invalid email format';
}
if (!values.password) {
errors.password = 'Password is required';
} else if (values.password.length < 8) {
errors.password = 'Password must be at least 8 characters';
}
return errors;
}, []);
const errors = useMemo(() => validateForm(formData), [formData, validateForm]);
场景3:API 请求
const fetchData = useCallback(async (params) => {
try {
const response = await api.get('/data', { params });
setData(response.data);
} catch (error) {
console.error('Error fetching data:', error);
}
}, []);
useEffect(() => {
fetchData({ page: currentPage });
}, [currentPage, fetchData]);
📈 性能优化效果评估
使用 React DevTools Profiler
React DevTools Profiler 可以帮助我们分析组件的渲染性能:
- 打开 React DevTools
- 切换到 Profiler 标签
- 点击录制按钮
- 进行用户操作
- 停止录制并分析结果
性能指标
关注以下性能指标:
- 渲染时间:组件每次渲染所花费的时间
- 渲染次数:组件在一段时间内渲染的次数
- 内存使用:应用的内存占用情况
🎓 总结
React 的性能优化是一个持续的过程,需要根据具体的应用场景选择合适的优化策略:
- useMemo:缓存计算结果,避免重复计算
- useCallback:缓存函数引用,避免不必要的子组件渲染
- memo:优化组件渲染,避免不必要的重新渲染
通过合理使用这些工具,我们可以显著提升 React 应用的性能,提供更好的用户体验。记住,性能优化应该在确实存在性能问题时进行,避免过度优化带来的复杂性。
掌握这些性能优化技巧,将帮助你在开发大型 React 应用时游刃有余,构建出高性能、高质量的应用程序。