useMemo
缓存计算结果的利器 - 避免昂贵计算的重复执行
核心概述
在 React 组件中,每次状态或 props 变化都会触发组件重新渲染, 导致组件内的所有代码重新执行。如果其中包含昂贵的计算(如大数组过滤、复杂排序、数学运算), 即使输入数据没变,也会浪费计算资源,导致界面卡顿。
useMemo 通过缓存计算结果来解决这个问题。 它接收一个计算函数和依赖数组,只在依赖变化时才重新执行计算,其他渲染直接返回缓存的结果。 这让你既能保持代码的响应式,又能避免不必要的性能开销。
核心机制:React 会在首次渲染时执行计算并缓存结果, 后续渲染时检查依赖数组中的值是否变化。如果依赖未变,直接返回缓存值,跳过计算; 如果依赖变了,重新计算并更新缓存。这种"按需计算"的策略是 React 性能优化的核心。
适用场景:当组件中有昂贵的计算、需要避免子组件不必要的渲染、 或需要确保对象引用稳定时使用 useMemo。但不要过度优化——只有当性能测试表明某个计算确实是瓶颈时才使用。
💡 心智模型
将 useMemo 想象成一个"智能备忘录":
- • 首次计算:写下问题和答案,存入备忘录
- • 后续查询:先检查问题中的数字(依赖)是否变化
- • 命中缓存:如果数字没变,直接读取备忘录中的答案
- • 重新计算:如果数字变了,重新计算并更新备忘录
关键:useMemo 的价值在于"跳过计算",而非"跳过渲染"。组件仍然会重新渲染, 但昂贵的计算被跳过了。
技术规格
类型签名
function useMemo<T>( calculateValue: () => T, dependencies: DependencyList ): T // 泛型 T 是缓存值的类型 // calculateValue 必须是纯函数,返回缓存值 // dependencies 是依赖数组,只有数组中的值变化时才重新计算
参数说明
| 参数 | 类型 | 说明 |
|---|---|---|
calculateValue | () => T | 计算缓存值的纯函数。必须返回一个值,不能接收参数。 函数只在依赖变化时执行。 |
dependencies | DependencyList | 依赖数组。React 使用 Object.is 比较数组中每个值与上次渲染的值。 只要有一个值变化,就会重新执行 calculateValue。 |
返回值
| 返回值 | 类型 | 说明 |
|---|---|---|
| 缓存值 | T | 首次渲染返回 calculateValue() 的执行结果, 后续渲染返回缓存的值(除非依赖变化)。 |
运行机制
依赖比较:React 使用 Object.is 比较依赖数组中的每个值。 对于基本类型,比较值是否相等;对于对象/数组,比较引用是否相同。 这意味着如果依赖是对象,必须确保对象引用稳定(配合 useMemo 或 useState)。
缓存失效:当任何一个依赖变化时,React 会在当前渲染中执行 calculateValue, 并将结果缓存用于下次渲染。需要注意的是,计算是同步执行的,如果计算太慢会阻塞渲染。
引用稳定性:useMemo 返回的值在依赖不变时保持同一个引用, 这对于传递给子组件作为 props 很重要。稳定的引用可以避免子组件触发不必要的渲染。
实战演练
1. 基础用法:过滤大数组
最常见的场景是过滤或排序大量数据,避免每次渲染都重新计算:
import { useMemo } from 'react';
interface Product {
id: string;
name: string;
category: string;
price: number;
}
interface ProductListProps {
products: Product[];
categoryFilter: string;
maxPrice?: number;
}
export function ProductList({ products, categoryFilter, maxPrice }: ProductListProps) {
// ✅ 只在 products 或 categoryFilter 变化时重新过滤
const filteredProducts = useMemo(() => {
console.log('过滤产品列表...'); // 可以观察何时重新计算
return products.filter(product => {
const matchCategory = product.category === categoryFilter;
const matchPrice = !maxPrice || product.price <= maxPrice;
return matchCategory && matchPrice;
});
}, [products, categoryFilter, maxPrice]);
return (
<div>
<h2>产品列表 ({filteredProducts.length})</h2>
<ul>
{filteredProducts.map(product => (
<li key={product.id}>
{product.name} - ¥{product.price}
</li>
))}
</ul>
</div>
);
}2. 生产级案例:复杂计算优化
展示如何缓存复杂的数学计算,避免在每次渲染时重复执行:
import { useMemo, useState } from 'react';
interface DataPoint {
x: number;
y: number;
}
interface StatisticsResult {
mean: number;
median: number;
standardDeviation: number;
variance: number;
}
export function StatisticsCalculator({ data }: { data: DataPoint[] }) {
const [showDetails, setShowDetails] = useState(false);
// ✅ 缓存复杂的统计计算
const statistics = useMemo((): StatisticsResult => {
console.log('计算统计数据...');
if (data.length === 0) {
return { mean: 0, median: 0, standardDeviation: 0, variance: 0 };
}
// 1. 计算平均值
const sum = data.reduce((acc, point) => acc + point.y, 0);
const mean = sum / data.length;
// 2. 计算方差
const squaredDiffs = data.map(point => Math.pow(point.y - mean, 2));
const variance = squaredDiffs.reduce((acc, val) => acc + val, 0) / data.length;
// 3. 计算标准差
const standardDeviation = Math.sqrt(variance);
// 4. 计算中位数
const sortedValues = [...data].sort((a, b) => a.y - b.y);
const mid = Math.floor(sortedValues.length / 2);
const median = sortedValues.length % 2 !== 0
? sortedValues[mid].y
: (sortedValues[mid - 1].y + sortedValues[mid].y) / 2;
return { mean, median, standardDeviation, variance };
}, [data]);
return (
<div className="p-6 bg-white rounded-lg shadow">
<h2 className="text-2xl font-bold mb-4">统计分析</h2>
{/* 基础统计 */}
<div className="space-y-2">
<p>数据点数量: {data.length}</p>
<p>平均值: {statistics.mean.toFixed(2)}</p>
<p>中位数: {statistics.median.toFixed(2)}</p>
</div>
{/* 切换详细显示 - 不会触发重新计算 statistics */}
<button
onClick={() => setShowDetails(!showDetails)}
className="mt-4 px-4 py-2 bg-blue-500 text-white rounded"
>
{showDetails ? '隐藏' : '显示'}详细统计
</button>
{/* 详细统计 - 使用缓存的值 */}
{showDetails && (
<div className="mt-4 p-4 bg-gray-50 rounded space-y-2">
<p>方差: {statistics.variance.toFixed(2)}</p>
<p>标准差: {statistics.standardDeviation.toFixed(2)}</p>
</div>
)}
</div>
);
}3. 生产级案例:避免子组件渲染
使用 useMemo 稳定对象引用,避免子组件因为父组件的无关状态变化而重新渲染:
import { useMemo, useState } from 'react';
interface User {
id: string;
name: string;
avatar: string;
}
interface UserProfileProps {
user: User;
onEdit: (user: User) => void;
}
// ✅ 使用 React.memo 优化子组件
const UserProfile = React.memo(({ user, onEdit }: UserProfileProps) => {
console.log('UserProfile 渲染'); // 观察渲染次数
return (
<div className="p-4 border rounded">
<img src={user.avatar} alt={user.name} className="w-12 h-12 rounded-full" />
<h3 className="font-semibold">{user.name}</h3>
<button onClick={() => onEdit(user)} className="text-blue-500">
编辑
</button>
</div>
);
});
export function UserList({ users }: { users: User[] }) {
const [filter, setFilter] = useState('');
const [otherState, setOtherState] = useState(0);
// ❌ 错误:每次渲染都创建新函数,导致 UserProfile 重新渲染
// const handleEdit = (user: User) => {
// console.log('编辑用户:', user);
// };
// ✅ 正确:使用 useCallback 稳定函数引用
const handleEdit = useMemo(() => (user: User) => {
console.log('编辑用户:', user);
}, []);
// ✅ 过滤后的用户列表
const filteredUsers = useMemo(() => {
return users.filter(user =>
user.name.toLowerCase().includes(filter.toLowerCase())
);
}, [users, filter]);
return (
<div className="p-6">
<input
type="text"
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="搜索用户..."
className="w-full px-4 py-2 border rounded mb-4"
/>
{/* 这个按钮不会影响 UserProfile 的渲染 */}
<button onClick={() => setOtherState(otherState + 1)}>
其他状态: {otherState}
</button>
<div className="space-y-2 mt-4">
{filteredUsers.map(user => (
<UserProfile
key={user.id}
user={user}
onEdit={handleEdit}
/>
))}
</div>
</div>
);
}4. 生产级案例:防止依赖循环
在 useMemo 中返回对象,确保对象引用稳定,避免无限循环:
import { useMemo, useEffect, useState } from 'react';
interface ChartConfig {
width: number;
height: number;
padding: number;
colors: string[];
}
export function Chart({ data }: { data: number[] }) {
const [containerSize, setContainerSize] = useState({ width: 800, height: 600 });
// ❌ 错误:每次渲染都创建新对象
// const config = {
// width: containerSize.width,
// height: containerSize.height,
// padding: 20,
// colors: ['#FF6B6B', '#4ECDC4'],
// };
// ✅ 正确:使用 useMemo 缓存配置对象
const config = useMemo<ChartConfig>(() => ({
width: containerSize.width,
height: containerSize.height,
padding: 20,
colors: ['#FF6B6B', '#4ECDC4'],
}), [containerSize.width, containerSize.height]);
// ✅ 因为 config 是稳定的,这个 effect 不会无限循环
useEffect(() => {
console.log('初始化图表,配置:', config);
// 初始化图表库...
}, [config]); // 依赖 config 是安全的
// 模拟容器尺寸变化
useEffect(() => {
const handleResize = () => {
setContainerSize({
width: window.innerWidth - 40,
height: window.innerHeight - 200,
});
};
window.addEventListener('resize', handleResize);
handleResize(); // 初始尺寸
return () => window.removeEventListener('resize', handleResize);
}, []);
return (
<div>
<h3>图表配置:</h3>
<pre>{JSON.stringify(config, null, 2)}</pre>
<div style={{ width: config.width, height: config.height, backgroundColor: '#f0f0f0' }}>
{/* 图表渲染 */}
<p>图表区域 ({config.width}x{config.height})</p>
</div>
</div>
);
}避坑指南
陷阱 1: 过度使用 useMemo
问题:对所有计算都使用 useMemo,包括简单的运算。 这会增加代码复杂度,甚至可能降低性能(缓存比较也有成本)。
// ❌ 过度优化:简单计算不需要 useMemo
function Component({ a, b }) {
const sum = useMemo(() => a + b, [a, b]);
const product = useMemo(() => a * b, [a, b]);
const isPositive = useMemo(() => a > 0, [a]);
return <div>{sum} {product} {isPositive}</div>;
}// ✅ 正确:简单计算直接在渲染中执行
function Component({ a, b }) {
// JavaScript 引擎对这些基本运算优化得很好
return <div>{a + b} {a * b} {a > 0}</div>;
}
// ✅ 使用 useMemo 的场景:O(n) 或更复杂的计算
function Component({ items }) {
const sorted = useMemo(() => {
// 这是一个 O(n log n) 的排序操作
return [...items].sort((a, b) => a.value - b.value);
}, [items]);
return <div>{sorted.map(...)}</div>;
}经验法则:只有当计算的复杂度是 O(n) 或更高,或者性能测试显示该计算是瓶颈时, 才使用 useMemo 优化。优先确保代码清晰,再考虑性能优化。
陷阱 2: 依赖数组不完整或错误
问题:依赖数组不完整会导致返回过期的缓存值, 依赖数组包含不必要的值会导致缓存失效,失去优化效果。
// ❌ 错误:缺少依赖 options
function Component({ data, options }) {
const result = useMemo(() => {
return processData(data, options); // 使用了 options 但未声明
}, [data]); // 缺少 options!
return <div>{result}</div>;
}
// ❌ 错误:依赖过多导致缓存频繁失效
function Component({ data, options, theme, locale, user }) {
const result = useMemo(() => {
return data.filter(item => item.value > options.min);
}, [data, options, theme, locale, user]); // theme, locale, user 不在计算中使用
return <div>{result}</div>;
}// ✅ 正确:依赖数组包含所有使用的值
function Component({ data, options }) {
const result = useMemo(() => {
return processData(data, options);
}, [data, options]); // 完整的依赖
return <div>{result}</div>;
}
// ✅ 使用 ESLint 的 exhaustive-deps 规则自动检查
// eslint-disable-next-line react-hooks/exhaustive-deps陷阱 3: 在 useMemo 中执行副作用
问题:在 useMemo 的计算函数中执行副作用(如 API 调用、DOM 操作), 违反了纯函数原则,可能导致不可预测的行为。
// ❌ 错误:在 useMemo 中调用 API
function UserProfile({ userId }) {
const user = useMemo(() => {
// ❌ 不要在 useMemo 中执行副作用!
fetch('/api/users/' + userId).then(res => res.json());
// 这会导致每次渲染都发起请求,且返回值不是 Promise
return null;
}, [userId]);
return <div>{user?.name}</div>;
}// ✅ 正确:使用 useEffect 处理副作用
import { useEffect, useState } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
// ✅ 副作用应该在 useEffect 中
useEffect(() => {
let cancelled = false;
fetch('/api/users/' + userId)
.then(res => res.json())
.then(data => {
if (!cancelled) {
setUser(data);
}
});
return () => {
cancelled = true;
};
}, [userId]);
return <div>{user?.name}</div>;
}陷阱 4: 期望 useMemo 减少渲染次数
常见误区:认为使用 useMemo 后组件就不会重新渲染。 实际上,useMemo 只是跳过计算,组件仍然会重新渲染,只是使用了缓存的值。
function Component({ count }) {
const expensiveValue = useMemo(() => {
console.log('执行昂贵计算...');
return count * 1000;
}, [count]);
// 即使 expensiveValue 被缓存,组件仍然会重新渲染
console.log('组件渲染');
return <div>{expensiveValue}</div>;
}
// 执行流程:
// 1. count 变化触发重新渲染
// 2. 组件函数执行,console.log('组件渲染')
// 3. useMemo 检查依赖,如果 count 没变,跳过计算,返回缓存值
// 4. 组件返回 JSX
// 5. React 更新 DOM
// 关键:组件函数仍然执行了,只是跳过了 useMemo 中的计算心智模型纠正:useMemo 的价值在于"跳过计算",而非"跳过渲染"。 如果要避免子组件渲染,需要结合 React.memo 和 useMemo 稳定 props 引用。
最佳实践
✅ 推荐模式
- 对 O(n) 或更复杂的计算使用 useMemo
- 使用 ESLint exhaustive-deps 规则检查依赖
- 用 useMemo 稳定传递给纯组件的 props
- 在 useMemo 中返回对象,避免依赖循环
- 先确保代码正确,再考虑性能优化
- 使用性能分析工具验证优化效果
❌ 避免模式
- 不要对简单计算使用 useMemo
- 不要在 useMemo 中执行副作用
- 不要忽略依赖数组的警告
- 不要期望 useMemo 减少组件渲染
- 不要为了优化而牺牲代码可读性
- 不要在 useMemo 中修改外部状态
📊 useMemo vs 直接计算
| 场景 | 直接计算 | useMemo |
|---|---|---|
| 每次渲染 | 重新执行计算 | 跳过计算(使用缓存) |
| 首次渲染 | 执行计算 | 执行计算并缓存 |
| 依赖变化 | 重新执行 | 重新执行并更新缓存 |
| 内存开销 | 无 | 需要存储缓存值 |