useMemo
是 React Hooks 中用于性能优化的核心 API,它的核心作用是缓存计算结果,避免在每次渲染时重复执行昂贵的计算 。以下是 useMemo
的详细应用场景、代码示例及最佳实践:
一、useMemo
的核心机制
- 输入:一个函数(计算逻辑)和一个依赖项数组。
- 输出:当依赖项变化时,重新执行函数并返回新值;否则返回缓存值。
- 适用条件:当计算成本较高或需要稳定引用时使用。
jsx
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
二、useMemo
的 10 个典型应用场景
1. 复杂计算缓存
场景 :组件内部有耗时计算(如大数据过滤、排序、数学运算)。
示例:计算斐波那契数列
jsx
const fibonacci = (n) => {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
};
const Component = ({ n }) => {
// 未优化:每次渲染都重新计算(性能极差)
// const result = fibonacci(n);
// 优化后:仅当 n 变化时计算
const result = useMemo(() => fibonacci(n), [n]);
return <div>{result}</div>;
};
2. 避免子组件不必要的渲染
场景 :父组件传递引用类型(对象、数组、函数)给子组件时,避免因引用变化导致子组件重新渲染。
示例:传递配置对象
jsx
const Child = React.memo(({ config }) => { /* ... */ });
const Parent = () => {
const [count, setCount] = useState(0);
// 未优化:每次渲染生成新对象,导致 Child 重新渲染
// const config = { color: "red", size: 10 };
// 优化后:仅当依赖项变化时生成新对象
const config = useMemo(() => ({ color: "red", size: 10 }), []);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>Update Parent</button>
<Child config={config} />
</div>
);
};
3. 优化 Context 传递的值
场景 :当 Context 提供的数据是动态生成的引用类型时,避免消费者组件因引用变化而重新渲染。
示例:动态主题配置
jsx
const ThemeContext = createContext();
const ThemeProvider = ({ children }) => {
const [darkMode, setDarkMode] = useState(false);
// 未优化:每次渲染生成新对象,导致所有消费者重新渲染
// const theme = { darkMode, colors: darkMode ? darkColors : lightColors };
// 优化后:仅当 darkMode 变化时生成新对象
const theme = useMemo(() => ({
darkMode,
colors: darkMode ? darkColors : lightColors
}), [darkMode]);
return (
<ThemeContext.Provider value={theme}>
{children}
</ThemeContext.Provider>
);
};
4. 格式化或数据处理
场景 :数据转换(如日期格式化、国际化、数据聚合)。
示例:日期格式化
jsx
const formatDate = (date) => {
// 复杂的格式化逻辑
return new Intl.DateTimeFormat('en-US').format(date);
};
const Component = ({ timestamp }) => {
// 未优化:每次渲染都格式化
// const formattedDate = formatDate(timestamp);
// 优化后:仅当 timestamp 变化时格式化
const formattedDate = useMemo(() => formatDate(timestamp), [timestamp]);
return <div>{formattedDate}</div>;
};
5. 动态样式生成
场景 :根据状态生成复杂 CSS 样式对象。
示例:动态样式计算
jsx
const Component = ({ isActive }) => {
// 未优化:每次渲染生成新样式对象
// const style = {
// color: isActive ? 'red' : 'black',
// transform: `scale(${isActive ? 1.2 : 1})`
// };
// 优化后:仅当 isActive 变化时生成新对象
const style = useMemo(() => ({
color: isActive ? 'red' : 'black',
transform: `scale(${isActive ? 1.2 : 1})`
}), [isActive]);
return <div style={style}>Dynamic Style</div>;
};
6. 大型列表或表格中的行渲染
场景 :渲染包含复杂行数据的列表时,避免每次渲染重新计算行数据。
示例:表格行数据预处理
jsx
const Table = ({ data, sortKey }) => {
// 未优化:每次渲染都重新排序
// const sortedData = data.sort((a, b) => a[sortKey] - b[sortKey]);
// 优化后:仅当 data 或 sortKey 变化时排序
const sortedData = useMemo(() =>
[...data].sort((a, b) => a[sortKey] - b[sortKey]),
[data, sortKey]
);
return (
<table>
{sortedData.map(row => <TableRow key={row.id} data={row} />)}
</table>
);
};
7. 避免重复渲染中的副作用
场景 :当某个值需要传递给副作用(如 useEffect
)且需要稳定引用时。
示例:依赖稳定引用的副作用
jsx
const Component = ({ id }) => {
// 未优化:每次渲染生成新配置对象,导致 useEffect 重复执行
// const config = { id, type: "FETCH" };
// 优化后:仅当 id 变化时生成新配置
const config = useMemo(() => ({ id, type: "FETCH" }), [id]);
useEffect(() => {
fetchData(config);
}, [config]);
return <div>...</div>;
};
8. 动态导入组件(Code Splitting)
场景 :按需加载组件时,避免重复动态导入。
示例:动态加载弹窗组件
jsx
const LazyModal = React.lazy(() => import('./Modal'));
const Component = ({ showModal }) => {
// 未优化:每次渲染都重新生成 JSX
// const modal = showModal ? <LazyModal /> : null;
// 优化后:仅在 showModal 变化时重新渲染
const modal = useMemo(() =>
showModal ? <LazyModal /> : null,
[showModal]
);
return <div>{modal}</div>;
};
9. 避免闭包中的过时值
场景 :在回调函数中需要访问最新状态时,结合 useRef
使用。
示例:定时器中的最新状态
jsx
const Component = () => {
const [count, setCount] = useState(0);
const countRef = useRef(count);
// 使用 useMemo 缓存回调函数,避免闭包陷阱
const updateCount = useMemo(() => () => {
console.log("Current count:", countRef.current);
}, []);
useEffect(() => {
countRef.current = count; // 同步最新值到 ref
}, [count]);
useEffect(() => {
const timer = setInterval(updateCount, 1000);
return () => clearInterval(timer);
}, [updateCount]);
return <button onClick={() => setCount(c => c + 1)}>Increment</button>;
};
10. 优化自定义 Hook 的输出
场景 :自定义 Hook 返回复杂数据时,避免调用组件不必要的更新。
示例:自定义数据获取 Hook
jsx
const useFetchData = (url) => {
const [data, setData] = useState(null);
useEffect(() => {
fetch(url).then(res => res.json()).then(setData);
}, [url]);
// 未优化:每次返回新对象,导致使用该 Hook 的组件重新渲染
// return { data, isLoading: !data };
// 优化后:稳定返回值引用
return useMemo(() => ({ data, isLoading: !data }), [data]);
};
三、useMemo
的误用场景
1. 简单计算
jsx
// ❌ 误用:计算过于简单,反而增加内存开销
const sum = useMemo(() => a + b, [a, b]);
// ✅ 正确:直接计算
const sum = a + b;
2. 依赖项不完整
jsx
// ❌ 误用:依赖项缺失,导致缓存值过时
const result = useMemo(() => a + b, [a]);
// ✅ 正确:包含所有依赖项
const result = useMemo(() => a + b, [a, b]);
3. 滥用引用稳定
jsx
// ❌ 误用:即使不需要稳定引用,也强制缓存
const handleClick = useMemo(() => () => { /* ... */ }, []);
// ✅ 正确:优先使用 useCallback
const handleClick = useCallback(() => { /* ... */ }, []);
四、最佳实践总结
- 按需优化 :先用 Profiler 定位性能瓶颈,再针对性使用
useMemo
。 - 权衡成本:计算成本 < 缓存开销时,避免使用。
- 依赖项精确:确保依赖项数组包含所有变化的变量。
- 避免嵌套滥用 :过度使用
useMemo
可能导致代码可读性下降。
通过合理应用 useMemo
,可以有效减少不必要的计算和渲染,提升 React 应用性能。核心原则是:在计算成本高或需要稳定引用时使用,避免在简单场景中滥用。